git ssb

1+

Matt McKegg / mutant



Tree: bf170bc7b3672405705146b57533b5850f041abc

Files: bf170bc7b3672405705146b57533b5850f041abc / computed.js

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

Built with git-ssb-web