git ssb

1+

Matt McKegg / mutant



Tree: c4f4b8e26329eb68faa7079102acbbffaf9679ca

Files: c4f4b8e26329eb68faa7079102acbbffaf9679ca / computed.js

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

Built with git-ssb-web