Skip to content

Commit c6aec85

Browse files
committed
Added releaseDependencies and removed Observable.concat and Observable#each
1 parent 179c604 commit c6aec85

File tree

3 files changed

+236
-485
lines changed

3 files changed

+236
-485
lines changed

main.coffee

+230
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
###
2+
Observable
3+
==========
4+
5+
`Observable` allows for observing arrays, functions, and objects.
6+
7+
Function dependencies are automagically observed.
8+
9+
Standard array methods are proxied through to the underlying array.
10+
11+
###
12+
13+
"use strict"
14+
15+
module.exports = Observable = (value, context) ->
16+
17+
# Return the object if it is already an observable object.
18+
return value if typeof value?.observe is "function"
19+
20+
# Maintain a set of listeners to observe changes and provide a helper to notify each observer.
21+
listeners = []
22+
notify = (newValue) ->
23+
copy(listeners).forEach (listener) ->
24+
listener(newValue)
25+
26+
# If `value` is a function compute dependencies and listen to observables that it depends on.
27+
if typeof value is 'function'
28+
fn = value
29+
30+
# Our return function is a function that holds only a cached value which is updated when it's dependencies change.
31+
# The `magicDependency` call is so other functions can depend on this computed function the same way we depend on other types of observables.
32+
self = ->
33+
# Automagic dependency observation
34+
magicDependency(self)
35+
36+
return value
37+
38+
# We expose releaseDependencies so that
39+
self.releaseDependencies = ->
40+
self._observableDependencies?.forEach (observable) ->
41+
observable.stopObserving changed
42+
43+
# We need to recompute our dependencies whenever any observable value that our function depends on changes. We keep a set
44+
# of observables (so we don't needlessly recompute the same ones multiple times). When a dependency changes we recompute
45+
# the new set of dependencies and unsubscribe from the old set.
46+
changed = ->
47+
observableDependencies = new Set
48+
49+
value = tryCallWithFinallyPop observableDependencies, fn, context
50+
51+
self.releaseDependencies()
52+
self._observableDependencies = observableDependencies
53+
observableDependencies.forEach (observable) ->
54+
observable.observe changed
55+
56+
notify(value)
57+
58+
changed()
59+
60+
else
61+
# When called with zero arguments it is treated as a getter. When called with one argument it is treated as a setter.
62+
# Changes to the value will trigger notifications. The value is always returned.
63+
self = (newValue) ->
64+
if arguments.length > 0
65+
if value != newValue
66+
value = newValue
67+
68+
notify(newValue)
69+
else
70+
# Automagic dependency observation
71+
magicDependency(self)
72+
73+
return value
74+
75+
# Non-computed observables have no dependencies, releasing them is a non-operation.
76+
self.releaseDependencies = noop
77+
78+
# If the value is an array then proxy array methods and add notifications to mutation events.
79+
if Array.isArray(value)
80+
[
81+
"concat"
82+
"every"
83+
"filter"
84+
"forEach"
85+
"indexOf"
86+
"join"
87+
"lastIndexOf"
88+
"map"
89+
"reduce"
90+
"reduceRight"
91+
"slice"
92+
"some"
93+
].forEach (method) ->
94+
self[method] = (args...) ->
95+
magicDependency(self)
96+
value[method](args...)
97+
98+
[
99+
"pop"
100+
"push"
101+
"reverse"
102+
"shift"
103+
"splice"
104+
"sort"
105+
"unshift"
106+
].forEach (method) ->
107+
self[method] = (args...) ->
108+
returnValue = value[method](args...)
109+
notify(value)
110+
return returnValue
111+
112+
# Provide length on a best effort basis because older browsers choke
113+
if PROXY_LENGTH
114+
Object.defineProperty self, 'length',
115+
get: ->
116+
magicDependency(self)
117+
value.length
118+
set: (length) ->
119+
returnValue = value.length = length
120+
notify(value)
121+
return returnValue
122+
123+
# Extra methods for array observables
124+
extend self,
125+
each: (callback) ->
126+
self.forEach (item, index) ->
127+
callback.call(item, item, index, self)
128+
129+
return self
130+
131+
# Remove an element from the array and notify observers of changes.
132+
remove: (object) ->
133+
index = value.indexOf(object)
134+
135+
if index >= 0
136+
returnValue = value.splice(index, 1)[0]
137+
notify(value)
138+
return returnValue
139+
140+
get: (index) ->
141+
magicDependency(self)
142+
value[index]
143+
144+
first: ->
145+
magicDependency(self)
146+
value[0]
147+
148+
last: ->
149+
magicDependency(self)
150+
value[value.length-1]
151+
152+
size: ->
153+
magicDependency(self)
154+
value.length
155+
156+
extend self,
157+
listeners: listeners
158+
159+
observe: (listener) ->
160+
listeners.push listener
161+
162+
stopObserving: (fn) ->
163+
remove listeners, fn
164+
165+
toggle: ->
166+
self !value
167+
168+
increment: (n=1) ->
169+
self value + n
170+
171+
decrement: (n=1) ->
172+
self value - n
173+
174+
toString: ->
175+
"Observable(#{value})"
176+
177+
return self
178+
179+
# Appendix
180+
# --------
181+
182+
extend = Object.assign
183+
184+
# Super hax for computing dependencies. This needs to be a shared global so that different bundled versions of observable libraries can interoperate.
185+
global.OBSERVABLE_ROOT_HACK = []
186+
187+
magicDependency = (self) ->
188+
observerSet = last(global.OBSERVABLE_ROOT_HACK)
189+
if observerSet
190+
observerSet.add self
191+
192+
193+
# Optimization: Keep the function containing the try-catch as small as possible.
194+
tryCallWithFinallyPop = (observableDependencies, fn, context) ->
195+
global.OBSERVABLE_ROOT_HACK.push(observableDependencies)
196+
197+
try
198+
fn.call(context)
199+
finally
200+
global.OBSERVABLE_ROOT_HACK.pop()
201+
202+
203+
# Check if we can proxy function length property.
204+
try
205+
Object.defineProperty (->), 'length',
206+
get: ->
207+
set: ->
208+
209+
PROXY_LENGTH = true
210+
catch
211+
PROXY_LENGTH = false
212+
213+
remove = (array, value) ->
214+
index = array.indexOf(value)
215+
216+
if index >= 0
217+
array.splice(index, 1)[0]
218+
219+
copy = (array) ->
220+
array.concat([])
221+
222+
last = (array) ->
223+
array[array.length - 1]
224+
225+
flatten = (array) ->
226+
array.reduce (a, b) ->
227+
a.concat(b)
228+
, []
229+
230+
noop = ->

0 commit comments

Comments
 (0)