UNPKG

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