git ssb

1+

Matt McKegg / mutant



Tree: 8e28bf396920836c01fd56e791f641c8a34675bb

Files: 8e28bf396920836c01fd56e791f641c8a34675bb / computed.js

6164 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.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
43 // when true, don't expand nested observables, just treat as values
44 this.passthru = opts && opts.passthru || null
45
46 this.context = opts && opts.context || {}
47 this.boundOnUpdate = this.onUpdate.bind(this)
48 this.boundUpdateNow = this.updateNow.bind(this)
49 this.boundUnlisten = this.unlisten.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.update()
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 && !this.listeners.length) {
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 if (!this.updating && !this.live) {
189 // temporarily become live to watch for changes until next cycle to stop
190 // potential double refresh and handle weird race conditions
191 this.listen() // triggers update
192 setImmediate(this.boundUnlisten) // only runs if no listeners have been added
193 }
194 return this.outputValue
195 },
196 broadcast: function () {
197 // cache listeners in case modified during broadcast
198 var listeners = this.listeners.slice(0)
199 for (var i = 0, len = listeners.length; i < len; i++) {
200 listeners[i](this.outputValue)
201 }
202 }
203}
204
205function extendedComputed (observables, update) {
206 var live = false
207
208 var instance = computed(observables, function () {
209 return update()
210 }, {
211 onListen: function () { live = true },
212 onUnlisten: function () { live = false }
213 })
214
215 instance.checkUpdated = function () {
216 if (!live) {
217 update()
218 }
219 }
220
221 return instance
222}
223

Built with git-ssb-web