git ssb

1+

Matt McKegg / mutant



Tree: b9dcc7d4908103b141bd9701cec47b3eebd64d2b

Files: b9dcc7d4908103b141bd9701cec47b3eebd64d2b / computed.js

5861 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 var release = this.opts.onListen()
88 if (typeof release === 'function') {
89 this.releases.push(release)
90 }
91 }
92 }
93 },
94 unlisten: function () {
95 if (this.live) {
96 this.live = false
97
98 if (this.releaseInner) {
99 this.releaseInner()
100 this.releaseInner = null
101 }
102
103 while (this.releases.length) {
104 this.releases.pop()()
105 }
106
107 if (this.opts && this.opts.onUnlisten) {
108 this.opts.onUnlisten()
109 }
110 }
111 },
112 update: function () {
113 var changed = false
114 for (var i = 0, len = this.observables.length; i < len; i++) {
115 var newValue = resolve(this.observables[i])
116 if (!isSame(newValue, this.values[i], this.comparer)) {
117 changed = true
118 this.values[i] = newValue
119 }
120 }
121
122 if (changed || !this.initialized) {
123 this.initialized = true
124 var newComputedValue = this.lambda.apply(this.context, this.values)
125
126 if (newComputedValue === computed.NO_CHANGE) {
127 return false
128 }
129
130 if (!isSame(newComputedValue, this.computedValue, this.comparer)) {
131 if (this.releaseInner) {
132 this.releaseInner()
133 this.inner = this.releaseInner = null
134 }
135
136 this.computedValue = newComputedValue
137
138 if (isObservable(newComputedValue)) {
139 // handle returning observable from computed
140 this.outputValue = newComputedValue()
141 this.inner = newComputedValue
142 if (this.live) {
143 this.releaseInner = this.inner(this.onInnerUpdate.bind(this, this.inner))
144 }
145 } else {
146 this.outputValue = this.computedValue
147 }
148 return true
149 }
150 }
151 return false
152 },
153 onUpdate: function () {
154 if (this.opts && this.opts.nextTick) {
155 if (!this.updating) {
156 this.updating = true
157 setImmediate(this.boundUpdateNow)
158 }
159 } else {
160 this.updateNow()
161 }
162 },
163 onInnerUpdate: function (obs, value) {
164 if (obs === this.inner) {
165 if (!isSame(value, this.outputValue, this.comparer)) {
166 this.outputValue = value
167 this.broadcast()
168 }
169 }
170 },
171 updateNow: function () {
172 this.updating = false
173 if (this.update()) {
174 this.broadcast()
175 }
176 },
177 getValue: function () {
178 if (!this.live || this.lazy || this.updating) {
179 this.lazy = false
180 if (this.opts && this.opts.nextTick && this.live && this.lazy) {
181 this.onUpdate() // use cached value to make more responsive
182 } else {
183 this.update()
184 }
185 if (this.inner) {
186 this.outputValue = resolve(this.inner)
187 }
188 }
189 return this.outputValue
190 },
191 broadcast: function () {
192 // cache listeners in case modified during broadcast
193 var listeners = this.listeners.slice(0)
194 for (var i = 0, len = listeners.length; i < len; i++) {
195 listeners[i](this.outputValue)
196 }
197 }
198}
199
200function extendedComputed (observables, update) {
201 var live = false
202 var lazy = false
203
204 var instance = computed(observables, function () {
205 return update()
206 }, {
207 onListen: function () { live = lazy = true },
208 onUnlisten: function () { live = false }
209 })
210
211 instance.checkUpdated = function () {
212 if (!live || lazy) {
213 lazy = false
214 update()
215 }
216 }
217
218 return instance
219}
220

Built with git-ssb-web