-
Notifications
You must be signed in to change notification settings - Fork 0
/
domvm-mobx.js
277 lines (227 loc) · 9.13 KB
/
domvm-mobx.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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/*! domvm-MobX v1.0.1 - MIT License - https://github.com/domvm/domvm-mobx */
(function(domvm, mobx, undefined) {
"use strict";
// UTILS:
function noop() {}
// We want the same checks as domvm:
function isPlainObj(val) { // See: https://github.com/domvm/domvm/blob/3.4.8/src/utils.js#L13
return val != undefined && val.constructor === Object; // && typeof val === "object"
}
function isFunc(val) { // See: https://github.com/domvm/domvm/blob/3.4.8/src/utils.js#L26
return typeof val === "function";
}
// DESIGN:
//
// We create a new MobX Reaction for each observer domvm vm (ViewModel).
// The Reaction is used to track the observables used by the render() method. It becomes stale
// when one of the tracked observables changes. Upon becoming stale, the default becameStale() hook
// schedules an async redraw of the vm (the user can setup its own becameStale() hook to change
// the default behavior).
// Lazy rendering: re-rendering is executed only when the observer is stale, which is checked by
// its diff.eq() function. (Rendering can be forced by the user diff() function if setup.)
// Reaction lifecycle: the Reaction is created at the beginning of the first render (ie. just before
// mounting), and destroyed during willUnmount(), which allows it to be reclaimed by the GC. But
// because the vm can be reused, we recreate the Reaction during the render() if we detect that
// the vm is reused.
// Conclusion: we need to replace four methods/hooks on every observer vm: diff.eq(), init(), render()
// and willUnmount(). And we also need to add one hook: becameStale().
// Notes:
// - There is no way to know that a vm is being reused before the execution of its diff()
// method (the willMount() hook is fired after the diff() and the render() methods).
// - The Reaction must be destroyed explicitly to prevent wasting computations and resources.
//
// Links:
// - domvm ViewModel: https://github.com/domvm/domvm/blob/master/src/view/ViewModel.js
// - MobX Reaction: https://github.com/mobxjs/mobx/blob/5.6.0/src/core/reaction.ts
// - Inspirations:
// - MobX bindings for Inferno: https://github.com/infernojs/inferno/blob/master/packages/inferno-mobx/src/observer.ts
// - mobx-observer (universal bindings): https://github.com/capaj/mobx-observer/blob/master/observer.js
// - MobX bindings for Preact: https://github.com/mobxjs/mobx-preact
// Turns a vm into an observer (ie. into a reactive view vm):
function initvm(vm, reactionName) {
// Uncomment if you need to find all unkeyed vm:
//if (vm.key === undefined) console.warn("Unkeyed reactive view:", reactionName, vm);
var hooks = vm.hooks || (vm.hooks = {});
vm._mobxObserver = {
// The reaction name, for debugging:
name: reactionName,
// The Reaction instance:
reaction: undefined,
// If the current view is stale and need (re-)rendering:
stale: true,
// The original diff.eq() if any:
eq: vm.diff && vm.diff.eq, // Since domvm 3.4.7
// The original render():
render: vm.render,
// The original hook willUnmount():
willUnmount: hooks.willUnmount,
};
// The user can prevent the default becameStale() if he did setup its own function,
// or if he did set it to false (note: this also checks for null because domvm often
// uses null for undefined):
if (hooks.becameStale == undefined)
hooks.becameStale = becameStale;
var valFn = vm.diff ? vm.diff.val : noop;
vm.config({diff: {val: valFn, eq: eq}}); // "vm.config()" has an alias "vm.cfg()" since domvm v3.4.7
vm.render = render;
hooks.willUnmount = willUnmount;
}
// Creates the observer Reaction:
function setReaction(vm) {
var observerData = vm._mobxObserver;
// Useful during development:
if (observerData.reaction)
throw Error("Reaction already set.");
observerData.stale = true;
observerData.reaction = new mobx.Reaction(observerData.name, function() {
observerData.stale = true;
if (vm.hooks.becameStale)
vm.hooks.becameStale(vm, vm.data);
});
// The reaction should be started right after creation. (See: https://github.com/mobxjs/mobx/blob/5.6.0/src/core/reaction.ts#L35)
// But it doesn't seem to be mandatory... ?
// Not doing it, as that would trigger becameStale() and a vm.redraw() right now !
// In case we need it, see convoluted implementation of fireImmediately in MobX autorun(): https://github.com/mobxjs/mobx/blob/5.6.0/src/api/autorun.ts#L146
//observerData.reaction.schedule();
}
// Destroys the observer Reaction:
function unsetReaction(vm) {
var observerData = vm._mobxObserver;
// Useful during development:
if (!observerData.reaction)
throw Error("Reaction already unset.");
observerData.reaction.dispose();
observerData.reaction = undefined;
}
// The default becameStale() assigned to each observer vm's hooks
function becameStale(vm) {
vm.redraw();
}
// The diff.eq() assigned to each observer vm:
function eq(vm) {
var observerData = vm._mobxObserver;
if (observerData.stale)
return false; // Re-render.
else if (observerData.eq)
return observerData.eq.apply(this, arguments); // Let diff() choose.
else
return true; // By default: no re-render.
}
// The render() wrapper assigned to each observer vm:
function render(vm) {
var observerData = vm._mobxObserver,
that = this,
args = arguments,
result;
// If vm was unmounted and is now being reused:
if (!observerData.reaction)
setReaction(vm);
// Note: the following is allowed to run by MobX even if the reaction is not stale:
observerData.reaction.track(function() {
mobx._allowStateChanges(false, function() {
result = observerData.render.apply(that, args);
});
});
observerData.stale = false;
return result;
}
// The willUnmount() wrapper assigned to each observer vm's hooks:
function willUnmount(vm) {
unsetReaction(vm);
var _willUnmount = vm._mobxObserver.willUnmount;
if (_willUnmount)
_willUnmount.apply(this, arguments);
}
// Replaces the init() with our own init():
function wrapInit(target, reactionName) {
target.init = (function(init) {
return function(vm) {
if (init)
init.apply(this, arguments);
initvm(vm, reactionName);
};
})(target.init);
}
// Replaces the init() with our own init(), but also checks that init() was not already replaced.
function wrapInitOnce(target, reactionName) {
if (!target.init || !target.init._mobxObserver) {
wrapInit(target, reactionName);
target.init._mobxObserver = true;
}
}
// Turns a view into a domvm-MobX observer view:
function observer(name, view) {
// If no name provided for the observer:
if (view === undefined) {
view = name;
// Generate friendly name for debugging (See: https://github.com/infernojs/inferno/blob/master/packages/inferno-mobx/src/observer.ts#L105)
name = view.displayName || view.name || (view.constructor && (view.constructor.displayName || view.constructor.name)) || '<View>';
}
// The name for the MobX Reaction, for debugging:
var reactionName = name + ".render()";
// We need to hook into the init() of the vm, just after that init() is executed,
// so that all the vm.config(...) have been executed on the vm.
// This is a bit complex depending on the type of the view.
// Refer to the ViewModel constructor for details:
// https://github.com/domvm/domvm/blob/3.4.8/src/view/ViewModel.js#L48
if (isPlainObj(view)) {
// view is an object: just set our own init on it.
wrapInit(view, reactionName);
}
else {
// view is a function: we can't do anything before it is executed, so we wrap it
// with a function that will set our own init later.
view = (function(view) {
return function(vm) {
var out = view.apply(this, arguments);
if (isFunc(out))
wrapInit(vm, reactionName);
else {
// In case multiple executions of view() returns the same object,
// we want to wrap init only once:
wrapInitOnce(out, reactionName);
}
return out;
};
})(view);
}
return view;
}
// Hook into domvm's redrawing to ensure all staled observers are correctly redrawn.
// Fixes issue #2: Redraw bug with domvm's drainQueue() (https://github.com/domvm/domvm-mobx/issues/2)
// In domvm, preventing a parent from re-rendering also prevents all its children.
// But with domvm-MobX we need discrete re-rendering, so we need to ensure that the children
// are actually re-rendered, even when their parents aren't.
// This hook requires domvm v3.4.8
domvm.config({
didRedraws: function forceRedrawOfStaledObservers(redrawQueue) {
// We want to redraw the children after the parents, so we sort them by depth:
var byDepth = [];
// Sorting:
redrawQueue.forEach(function(vm) {
// Only keep staled domvm-mobx observers (and check they were not unmounted)
if (vm._mobxObserver && vm._mobxObserver.stale && vm.node != undefined) {
var depth = 0,
parVm = vm;
while (parVm = parVm.parent())
depth++;
if (!byDepth[depth])
byDepth[depth] = [];
byDepth[depth].push(vm);
}
});
// Re-rendering in order:
for (var d = 0; d < byDepth.length; d++) {
if (byDepth[d]) {
byDepth[d].forEach(function(vm) {
// May have been redrawn or unmounted by a parent in the meanwhile:
if (vm._mobxObserver.stale && vm.node != undefined)
vm.redraw(true);
});
}
}
}
});
// EXPORTS:
domvm.mobxObserver = observer;
})(window.domvm, window.mobx);