git ssb

1+

Matt McKegg / mutant



Tree: 2b5e31cac39d3db81a26461d2d5ea9de451caf07

Files: 2b5e31cac39d3db81a26461d2d5ea9de451caf07 / computed.js

5527 bytesRaw
1/* A lazy binding take on computed */
2// - doesn't start watching observables until itself is watched, and then releases if unwatched
3// - avoids memory/watcher leakage
4// - attaches to inner observables if these are returned from value
5// - doesn't broadcast if value is same as last value (and is `value type` or observable - can't make assuptions about reference types)
6// - doesn't broadcast if value is computed.NO_CHANGE
7
8var resolve = require('./resolve')
9var isObservable = require('./is-observable')
10var isSame = require('./lib/is-same')
11
12module.exports = computed
13
14computed.NO_CHANGE = {}
15computed.extended = extendedComputed
16
17function computed (observables, lambda, opts) {
18 // opts: nextTick, comparer, context
19 var instance = new ProtoComputed(observables, lambda, opts)
20 return instance.MutantComputed.bind(instance)
21}
22
23// optimise memory usage
24function ProtoComputed (observables, lambda, opts) {
25 if (!Array.isArray(observables)) {
26 observables = [observables]
27 }
28 this.values = []
29 this.releases = []
30 this.computedValue = []
31 this.inner = null
32 this.updating = false
33 this.live = false
34 this.lazy = false
35 this.initialized = false
36 this.listeners = []
37 this.observables = observables
38 this.lambda = lambda
39 this.opts = opts
40 this.comparer = opts && opts.comparer || null
41 this.context = opts && opts.context || {}
42 this.boundOnUpdate = this.onUpdate.bind(this)
43 this.boundUpdateNow = this.updateNow.bind(this)
44}
45
46ProtoComputed.prototype = {
47 MutantComputed: function (listener) {
48 if (!listener) {
49 return this.getValue()
50 }
51
52 if (typeof listener !== 'function') {
53 throw new Error('Listeners must be functions.')
54 }
55
56 this.listeners.push(listener)
57 this.listen()
58
59 return this.removeListener.bind(this, listener)
60 },
61 removeListener: function (listener) {
62 for (var i = 0, len = this.listeners.length; i < len; i++) {
63 if (this.listeners[i] === listener) {
64 this.listeners.splice(i, 1)
65 break
66 }
67 }
68 if (!this.listeners.length) {
69 this.unlisten()
70 }
71 },
72 listen: function () {
73 if (!this.live) {
74 for (var i = 0, len = this.observables.length; i < len; i++) {
75 if (isObservable(this.observables[i])) {
76 this.releases.push(this.observables[i](this.boundOnUpdate))
77 }
78 }
79 if (this.inner) {
80 this.releaseInner = this.inner(this.onInnerUpdate.bind(this, this.inner))
81 }
82 this.live = true
83 this.lazy = true
84
85 if (this.opts && this.opts.onListen) {
86 this.opts.onListen()
87 }
88 }
89 },
90 unlisten: function () {
91 if (this.live) {
92 this.live = false
93
94 if (this.releaseInner) {
95 this.releaseInner()
96 this.releaseInner = null
97 }
98
99 while (this.releases.length) {
100 this.releases.pop()()
101 }
102
103 if (this.opts && this.opts.onUnlisten) {
104 this.opts.onUnlisten()
105 }
106 }
107 },
108 update: function () {
109 var changed = false
110 for (var i = 0, len = this.observables.length; i < len; i++) {
111 var newValue = resolve(this.observables[i])
112 if (!isSame(newValue, this.values[i], this.comparer)) {
113 changed = true
114 this.values[i] = newValue
115 }
116 }
117
118 if (changed || !this.initialized) {
119 this.initialized = true
120 var newComputedValue = this.lambda.apply(this.context, this.values)
121
122 if (newComputedValue === computed.NO_CHANGE) {
123 return false
124 }
125
126 if (!isSame(newComputedValue, this.computedValue, this.comparer)) {
127 if (this.releaseInner) {
128 this.releaseInner()
129 this.inner = this.releaseInner = null
130 }
131
132 if (isObservable(newComputedValue)) {
133 // handle returning observable from computed
134 this.computedValue = newComputedValue()
135 this.inner = newComputedValue
136 if (this.live) {
137 this.releaseInner = this.inner(this.onInnerUpdate.bind(this, this.inner))
138 }
139 } else {
140 this.computedValue = newComputedValue
141 }
142 return true
143 }
144 }
145 return false
146 },
147 onUpdate: function () {
148 if (this.opts && this.opts.nextTick) {
149 if (!this.updating) {
150 this.updating = true
151 setImmediate(this.boundUpdateNow)
152 }
153 } else {
154 this.updateNow()
155 }
156 },
157 onInnerUpdate: function (obs, value) {
158 if (obs === this.inner) {
159 if (!isSame(value, this.computedValue, this.comparer)) {
160 this.computedValue = value
161 this.broadcast()
162 }
163 }
164 },
165 updateNow: function () {
166 this.updating = false
167 if (this.update()) {
168 this.broadcast()
169 }
170 },
171 getValue: function () {
172 if (!this.live || this.lazy || this.updating) {
173 this.lazy = false
174 this.update()
175 if (this.inner) {
176 this.computedValue = resolve(this.inner)
177 }
178 }
179 return this.computedValue
180 },
181 broadcast: function () {
182 // cache listeners in case modified during broadcast
183 var listeners = this.listeners.slice(0)
184 for (var i = 0, len = listeners.length; i < len; i++) {
185 listeners[i](this.computedValue)
186 }
187 }
188}
189
190function extendedComputed (observables, update) {
191 var live = false
192 var lazy = false
193
194 var instance = computed(observables, function () {
195 return update()
196 }, {
197 onListen: function () { live = lazy = true },
198 onUnlisten: function () { live = false }
199 })
200
201 instance.checkUpdated = function () {
202 if (!live || lazy) {
203 lazy = false
204 update()
205 }
206 }
207
208 return instance
209}
210

Built with git-ssb-web