git ssb

1+

Matt McKegg / mutant



Tree: 8517593adc359f5dabe71f32777370c7e71159d3

Files: 8517593adc359f5dabe71f32777370c7e71159d3 / computed.js

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

Built with git-ssb-web