git ssb

1+

Matt McKegg / mutant



Tree: 52a53f6c954527bd2ff1b45d18bbf6679ef83038

Files: 52a53f6c954527bd2ff1b45d18bbf6679ef83038 / computed.js

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

Built with git-ssb-web