git ssb

1+

Matt McKegg / mutant



Tree: 30936e93cd05ef20992cd348fcf9d6a68059ea4c

Files: 30936e93cd05ef20992cd348fcf9d6a68059ea4c / computed.js

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

Built with git-ssb-web