Files: ba938d4692fe73ad9200093bf9c44381ab8373e1 / computed.js
3780 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 | |
11 | module.exports = computed |
12 | |
13 | computed.NO_CHANGE = {} |
14 | |
15 | function computed (observables, lambda) { |
16 | if (!Array.isArray(observables)) { |
17 | observables = [observables] |
18 | } |
19 | |
20 | var values = [] |
21 | var releases = [] |
22 | var computedValue = null |
23 | |
24 | var inner = null |
25 | var releaseInner = null |
26 | |
27 | var live = false |
28 | var lazy = false |
29 | var initialized = false |
30 | var listeners = [] |
31 | |
32 | var result = function (listener) { |
33 | if (!listener) { |
34 | return getValue() |
35 | } |
36 | |
37 | if (typeof listener !== 'function') { |
38 | throw new Error('Listeners must be functions.') |
39 | } |
40 | |
41 | listeners.push(listener) |
42 | listen() |
43 | |
44 | return function remove () { |
45 | for (var i = 0, len = listeners.length; i < len; i++) { |
46 | if (listeners[i] === listener) { |
47 | listeners.splice(i, 1) |
48 | break |
49 | } |
50 | } |
51 | if (!listeners.length) { |
52 | unlisten() |
53 | } |
54 | } |
55 | } |
56 | |
57 | return result |
58 | |
59 | // scoped |
60 | |
61 | function listen () { |
62 | if (!live) { |
63 | for (var i = 0, len = observables.length; i < len; i++) { |
64 | if (isObservable(observables[i])) { |
65 | releases.push(observables[i](onUpdate)) |
66 | } |
67 | } |
68 | if (inner) { |
69 | releaseInner = inner(onInnerUpdate) |
70 | } |
71 | live = true |
72 | lazy = true |
73 | } |
74 | } |
75 | |
76 | function unlisten () { |
77 | if (live) { |
78 | live = false |
79 | |
80 | if (releaseInner) { |
81 | releaseInner() |
82 | releaseInner = null |
83 | } |
84 | |
85 | while (releases.length) { |
86 | releases.pop()() |
87 | } |
88 | } |
89 | } |
90 | |
91 | function update () { |
92 | var changed = false |
93 | for (var i = 0, len = observables.length; i < len; i++) { |
94 | var newValue = resolve(observables[i]) |
95 | if (newValue !== values[i] || isReferenceType(newValue)) { |
96 | changed = true |
97 | values[i] = newValue |
98 | } |
99 | } |
100 | |
101 | if (changed || !initialized) { |
102 | initialized = true |
103 | var newComputedValue = lambda.apply(null, values) |
104 | |
105 | if (newComputedValue === computed.NO_CHANGE) { |
106 | return false |
107 | } |
108 | |
109 | if (newComputedValue !== computedValue || (isReferenceType(newComputedValue) && !isObservable(newComputedValue))) { |
110 | if (releaseInner) { |
111 | releaseInner() |
112 | inner = releaseInner = null |
113 | } |
114 | |
115 | if (isObservable(newComputedValue)) { |
116 | // handle returning observable from computed |
117 | computedValue = newComputedValue() |
118 | inner = newComputedValue |
119 | if (live) { |
120 | releaseInner = inner(onInnerUpdate) |
121 | } |
122 | } else { |
123 | computedValue = newComputedValue |
124 | } |
125 | return true |
126 | } |
127 | } |
128 | return false |
129 | } |
130 | |
131 | function onInnerUpdate (value) { |
132 | if (value !== computedValue || isReferenceType(computedValue)) { |
133 | computedValue = value |
134 | broadcast(listeners, computedValue) |
135 | } |
136 | } |
137 | |
138 | function onUpdate () { |
139 | if (update()) { |
140 | broadcast(listeners, computedValue) |
141 | } |
142 | } |
143 | |
144 | function getValue () { |
145 | if (!live || lazy) { |
146 | lazy = false |
147 | update() |
148 | } |
149 | return computedValue |
150 | } |
151 | } |
152 | |
153 | function isReferenceType (value) { |
154 | return typeof value === 'object' && value !== null |
155 | } |
156 | |
157 | function broadcast (listeners, value) { |
158 | // cache listeners in case modified during broadcast |
159 | listeners = listeners.slice(0) |
160 | for (var i = 0, len = listeners.length; i < len; i++) { |
161 | listeners[i](value) |
162 | } |
163 | } |
164 |
Built with git-ssb-web