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