git ssb

1+

Matt McKegg / mutant



Tree: 408284391d679877e10234efd7de780b0d75d121

Files: 408284391d679877e10234efd7de780b0d75d121 / computed.js

5590 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 = null
31 this.outputValue = null
32 this.inner = null
33 this.updating = false
34 this.live = false
35 this.lazy = false
36 this.initialized = false
37 this.listeners = []
38 this.observables = observables
39 this.lambda = lambda
40 this.opts = opts
41 this.comparer = opts && opts.comparer || null
42 this.context = opts && opts.context || {}
43 this.boundOnUpdate = this.onUpdate.bind(this)
44 this.boundUpdateNow = this.updateNow.bind(this)
45}
46
47ProtoComputed.prototype = {
48 MutantComputed: function (listener) {
49 if (!listener) {
50 return this.getValue()
51 }
52
53 if (typeof listener !== 'function') {
54 throw new Error('Listeners must be functions.')
55 }
56
57 this.listeners.push(listener)
58 this.listen()
59
60 return this.removeListener.bind(this, listener)
61 },
62 removeListener: function (listener) {
63 for (var i = 0, len = this.listeners.length; i < len; i++) {
64 if (this.listeners[i] === listener) {
65 this.listeners.splice(i, 1)
66 break
67 }
68 }
69 if (!this.listeners.length) {
70 this.unlisten()
71 }
72 },
73 listen: function () {
74 if (!this.live) {
75 for (var i = 0, len = this.observables.length; i < len; i++) {
76 if (isObservable(this.observables[i])) {
77 this.releases.push(this.observables[i](this.boundOnUpdate))
78 }
79 }
80 if (this.inner) {
81 this.releaseInner = this.inner(this.onInnerUpdate.bind(this, this.inner))
82 }
83 this.live = true
84 this.lazy = true
85
86 if (this.opts && this.opts.onListen) {
87 this.opts.onListen()
88 }
89 }
90 },
91 unlisten: function () {
92 if (this.live) {
93 this.live = false
94
95 if (this.releaseInner) {
96 this.releaseInner()
97 this.releaseInner = null
98 }
99
100 while (this.releases.length) {
101 this.releases.pop()()
102 }
103
104 if (this.opts && this.opts.onUnlisten) {
105 this.opts.onUnlisten()
106 }
107 }
108 },
109 update: function () {
110 var changed = false
111 for (var i = 0, len = this.observables.length; i < len; i++) {
112 var newValue = resolve(this.observables[i])
113 if (!isSame(newValue, this.values[i], this.comparer)) {
114 changed = true
115 this.values[i] = newValue
116 }
117 }
118
119 if (changed || !this.initialized) {
120 this.initialized = true
121 var newComputedValue = this.lambda.apply(this.context, this.values)
122
123 if (newComputedValue === computed.NO_CHANGE) {
124 return false
125 }
126
127 if (!isSame(newComputedValue, this.computedValue, this.comparer)) {
128 if (this.releaseInner) {
129 this.releaseInner()
130 this.inner = this.releaseInner = null
131 }
132
133 this.computedValue = newComputedValue
134
135 if (isObservable(newComputedValue)) {
136 // handle returning observable from computed
137 this.outputValue = newComputedValue()
138 this.inner = newComputedValue
139 if (this.live) {
140 this.releaseInner = this.inner(this.onInnerUpdate.bind(this, this.inner))
141 }
142 } else {
143 this.outputValue = this.computedValue
144 }
145 return true
146 }
147 }
148 return false
149 },
150 onUpdate: function () {
151 if (this.opts && this.opts.nextTick) {
152 if (!this.updating) {
153 this.updating = true
154 setImmediate(this.boundUpdateNow)
155 }
156 } else {
157 this.updateNow()
158 }
159 },
160 onInnerUpdate: function (obs, value) {
161 if (obs === this.inner) {
162 if (!isSame(value, this.outputValue, this.comparer)) {
163 this.outputValue = value
164 this.broadcast()
165 }
166 }
167 },
168 updateNow: function () {
169 this.updating = false
170 if (this.update()) {
171 this.broadcast()
172 }
173 },
174 getValue: function () {
175 if (!this.live || this.lazy || this.updating) {
176 this.lazy = false
177 this.update()
178 if (this.inner) {
179 this.outputValue = resolve(this.inner)
180 }
181 }
182 return this.outputValue
183 },
184 broadcast: function () {
185 // cache listeners in case modified during broadcast
186 var listeners = this.listeners.slice(0)
187 for (var i = 0, len = listeners.length; i < len; i++) {
188 listeners[i](this.outputValue)
189 }
190 }
191}
192
193function extendedComputed (observables, update) {
194 var live = false
195 var lazy = false
196
197 var instance = computed(observables, function () {
198 return update()
199 }, {
200 onListen: function () { live = lazy = true },
201 onUnlisten: function () { live = false }
202 })
203
204 instance.checkUpdated = function () {
205 if (!live || lazy) {
206 lazy = false
207 update()
208 }
209 }
210
211 return instance
212}
213

Built with git-ssb-web