-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathobserve.js
150 lines (131 loc) · 4.6 KB
/
observe.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/** @module decor/observe */
define([
"dcl/dcl",
"./schedule",
"./Notifier"
], function (
dcl,
schedule,
Notifier
) {
// Object.is() polyfill from Observable.js
function is(lhs, rhs) {
return lhs === rhs && (lhs !== 0 || 1 / lhs === 1 / rhs) || lhs !== lhs && rhs !== rhs;
}
function isObject(obj) {
return obj && typeof obj === "object" && !(obj instanceof HTMLElement) && !Array.isArray(obj);
}
// Deep compare oldObj to newObj and call notify() for properties that were changed or added in newObj.
function diff(oldObj, newObj, notify, prefix) {
for (var prop in newObj) {
if (!oldObj || !(prop in oldObj) || !is(oldObj[prop], newObj[prop])) {
if (isObject(oldObj[prop]) && isObject(newObj[prop])) {
diff(oldObj[prop], newObj[prop], notify, prefix + prop + ".");
} else {
notify(prefix + prop, oldObj ? oldObj[prop] : undefined, newObj ? newObj[prop] : undefined);
}
}
}
}
// Mapping from instrumented POJO to array of listeners to notify when that object is changed.
var map = new WeakMap();
// Call callback(prop, oldVal, newVal) whenever a property or nested property of the POJO is changed.
// Callback is synchronous.
function watchPojo(pojo, callback) {
// Array of functions to call whenever a property or nested property of the POJO is changed.
var callbacks = map.get(pojo);
// Function to call callbacks when a property has changed.
function notify(prop, oldVal, newVal) {
callbacks.forEach(function (watcher) {
watcher(prop, oldVal, newVal);
});
}
if (!callbacks) {
callbacks = [];
map.set(pojo, callbacks);
// Go through each property in the object, and convert it to a custom setter and getter.
Object.keys(pojo).forEach(function (prop) {
// Shadow value referenced by setter and getter.
var curVal = pojo[prop];
// If property is an object then set up listener on that object too.
var nestedWatcher;
if (isObject(curVal)) {
nestedWatcher = watchPojo(curVal, function (nestedProp, nestedOldVal, nestedNewVal) {
notify(prop + "." + nestedProp, nestedOldVal, nestedNewVal);
});
}
// Convert property into setter and getter.
Object.defineProperty(pojo, prop, {
enumerable: true,
set: function (newVal) {
// Ignore when property set to same value as before.
if (is(newVal, curVal)) {
return;
}
// If old value was an object, then remove listener on that object.
if (nestedWatcher) {
nestedWatcher.remove();
nestedWatcher = null;
}
// If new value is an object, set up listener on that object.
if (isObject(newVal)) {
nestedWatcher = watchPojo(newVal, function (nestedProp, nestedOldVal, nestedNewVal) {
notify(prop + "." + nestedProp, nestedOldVal, nestedNewVal);
});
}
// Save new value.
var oldVal = curVal;
curVal = newVal;
if (isObject(oldVal) && isObject(newVal)) {
// Recursive diff oldVal vs. newVal and send notifications of nested prop changes.
diff(oldVal, newVal, notify, prop + ".");
} else {
// Notify all listeners that property has changed.
notify(prop, oldVal, newVal);
}
},
get: function () {
return curVal;
}
});
});
}
callbacks.push(callback);
return {
remove: function () {
var idx = callbacks.indexOf(callback);
if (idx >= 0) {
callbacks.splice(idx, 1);
}
}
};
}
// Call `callback(oldVals)` whenever one or more properties on specified POJO are changed.
function observePojo(pojo, callback) {
var notifier = new Notifier(callback);
return watchPojo(pojo, notifier.notify.bind(notifier));
}
/**
* Call `callback(oldVals)` whenever one or more properties or nested properties on specified object are changed.
* Object can be a POJO or a decor/Stateful subclass (i.e. an Object with an `observe()` method).
* Similar to decor/Observable and decor/Stateful, but with key difference that it
* attaches to an existing object.
*
* A watched POJO should be updated the same way as usual, ex: `obj.foo = bar`, rather
* than a setter API like `obj.set("foo", bar)`.
*
* The callback is called asynchronously in the spirit of decor/Stateful#observe(),
* i.e. a single notification of all the properties that were changed since the last
* microtask.
*
* Also, like decor/Observable, callbacks are called in the order they were registered regardless
* of the order the objects are updated in.
*/
return function (obj, callback) {
if (typeof obj.observe === "function") {
return obj.observe(callback);
} else {
return observePojo(obj, callback);
}
};
});