Skip to content

Commit 0d78674

Browse files
committed
EBEAST: utilities.js: track this.dom_update() calls reactively
Since vuejs/vue#7573, Vue only tracks data dependencies during its VNode render() function which is unsuitable for drawing into DOM nodes (e.g. subsequent width/height patching by Vue will re-erase <canvas/> elements). The `dom_updates` Mixin now calls `this.dom_update()` for reliable rendering into DOM elements, *after* Vue has patched the DOM tree, and tracks dependencies during synchronous calls. Signed-off-by: Tim Janik <[email protected]>
1 parent 57f6a47 commit 0d78674

File tree

1 file changed

+93
-40
lines changed

1 file changed

+93
-40
lines changed

ebeast/utilities.js

+93-40
Original file line numberDiff line numberDiff line change
@@ -194,55 +194,108 @@ export function hyphenate (string) {
194194
}
195195

196196
/** Vue mixin to provide a `dom_create`, `dom_update`, `dom_destroy` hooks.
197-
* This mixin allowes async instance method callbacks for DOM element creation
198-
* (`this.dom_create()`), updates (`this.dom_update()`, also called right after
199-
* `this.dom_create()`) and destruction (`this.dom_destroy()`). It is ensured
200-
* that invocation of asnyc callbacks is serialized, so `dom_create` needs to
201-
* finish before `dom_update`, which in turn has to finish before subsequent
202-
* calls to `dom_update` or `dom_destroy`.
203-
* The Boolean member `this.dom_present` indicates whether DOM elements are
204-
* still accessible (e.g. via `this.$el` or `this.$refs`), which can change
205-
* at any `await` point in an async function.
206-
* The Boolean member `this.dom_destroying` indicates wether DOM elements are
207-
* being destroyed intermittingly, which can happen at any `await` point in
208-
* an async function.
197+
* This mixin calls instance method callbacks for DOM element creation
198+
* (`this.dom_create()`), updates (`this.dom_update()`,
199+
* and destruction (`this.dom_destroy()`).
200+
* If `dom_create` is an async function or returns a Promise, `dom_update`
201+
* calls are deferred until the returned Promise is resolved.
202+
*
203+
* Access to reactive properties during `dom_update` are tracked as dependencies,
204+
* watched by Vue, so future changes cause rerendering of the Vue component.
209205
*/
210206
vue_mixins.dom_updates = {
211207
beforeCreate: function () {
212-
console.assert (this.dom_handler_promise == undefined);
213-
this.dom_handler_promise = null;
214-
this.dom_present = false;
215-
this.dom_destroying = false;
216-
},
208+
console.assert (this.$dom_updates == undefined);
209+
// install $dom_updates helper on Vue instance
210+
this.$dom_updates = {
211+
// members
212+
promise: Promise.resolve(),
213+
destroying: false,
214+
pending: false, // dom_update pending
215+
unwatch: null,
216+
// methods
217+
chain_await: (promise_or_function) => {
218+
const result = promise_or_function instanceof Function ? promise_or_function() : promise_or_function;
219+
if (result instanceof Promise)
220+
this.$dom_updates.promise =
221+
this.$dom_updates.promise.then (async () => await result);
222+
},
223+
call_update: (resolve) => {
224+
/* Here we invoke `dom_update` with dependency tracking through $watch. In case
225+
* it is implemented as an async function, we await the returned promise to
226+
* serialize with future `dom_update` or `dom_destroy` calls. Note that
227+
* dependencies cannot be tracked beyond the first await point in `dom_update`.
228+
*/
229+
// Clear old $watch if any
230+
if (this.$dom_updates.unwatch)
231+
{
232+
this.$dom_updates.unwatch();
233+
this.$dom_updates.unwatch = null;
234+
}
235+
/* Note, if vm._watcher is triggered before the $watch from below, it'll re-render
236+
* the VNodes and then our watcher is triggered, which causes $forceUpdate() and the
237+
* VNode tree is rendered *again*. This causes multiple calles to updated(), too.
238+
*/
239+
let once = 0;
240+
const update_expr = vm => {
241+
if (once == 0)
242+
{
243+
const result = this.dom_update (this);
244+
if (result instanceof Promise)
245+
{
246+
// Note, async dom_update() looses reactivity…
247+
result.then (resolve);
248+
// console.warn ('dom_update() should not return Promise:', this);
249+
}
250+
else
251+
resolve();
252+
}
253+
return ++once; // always change return value and guard against subsequent calls
254+
};
255+
/* A note on $watch. Its `expOrFn` is called immediately, the retrun value and
256+
* dependencies are recorded. Later, once a dependency changes, its `expOrFn`
257+
* is called again, also recording return value and new dependencies.
258+
* If the return value changes, `callback` is invoked.
259+
* What we need for updating DOM elements, is:
260+
* a) the initial call with dependency recording which we use for (expensive) updating,
261+
* b) trigger $forceUpdate() once a dependency changes, without intermediate expensive updating.
262+
*/
263+
this.$dom_updates.unwatch = this.$watch (update_expr, this.$forceUpdate);
264+
},
265+
};
266+
}, // beforeCreate
217267
mounted: function () {
218-
this.dom_present = true;
219-
console.assert (this.dom_handler_promise == null);
220-
this.dom_handler_promise = (async () => {
221-
if (this.dom_create)
222-
await this.dom_create();
223-
}) ();
224-
if (this.dom_update)
225-
this.dom_handler_promise = this.dom_handler_promise.then (async () => {
226-
if (this.dom_present)
227-
await this.dom_update();
228-
});
268+
console.assert (this.$dom_updates);
269+
if (this.dom_create)
270+
this.$dom_updates.chain_await (this.dom_create());
271+
this.$forceUpdate(); // always trigger `dom_update` after `dom_create`
229272
},
230273
updated: function () {
231-
console.assert (this.dom_handler_promise);
232-
if (this.dom_update)
233-
this.dom_handler_promise = this.dom_handler_promise.then (async () => {
234-
if (this.dom_present)
235-
await this.dom_update();
236-
});
274+
console.assert (this.$dom_updates);
275+
/* If multiple $watch() instances are triggered by an update, Vue may re-render
276+
* and call updated() several times in a row. To avoid expensive intermediate
277+
* updates, we use this.$dom_updates.pending as guard.
278+
*/
279+
if (this.dom_update && !this.$dom_updates.pending)
280+
{
281+
this.$dom_updates.chain_await (new Promise (resolve => {
282+
// Wrap call_update() into a chained promise to serialize with dom_destroy
283+
this.$nextTick (() => {
284+
this.$dom_updates.pending = false;
285+
if (this.$dom_updates.destroying)
286+
resolve(); // No need for updates during destruction
287+
else
288+
this.$dom_updates.call_update (resolve);
289+
});
290+
}));
291+
this.$dom_updates.pending = true;
292+
}
237293
},
238294
beforeDestroy: function () {
239-
this.dom_present = false;
240-
this.dom_destroying = true;
241-
console.assert (this.dom_handler_promise);
295+
console.assert (this.$dom_updates);
296+
this.$dom_updates.destroying = true;
242297
if (this.dom_destroy)
243-
this.dom_handler_promise = this.dom_handler_promise.then (async () => {
244-
await this.dom_destroy();
245-
});
298+
this.$dom_updates.chain_await (() => this.dom_destroy());
246299
},
247300
};
248301

0 commit comments

Comments
 (0)