@@ -194,55 +194,108 @@ export function hyphenate (string) {
194
194
}
195
195
196
196
/** 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.
209
205
*/
210
206
vue_mixins . dom_updates = {
211
207
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
217
267
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`
229
272
} ,
230
273
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
+ }
237
293
} ,
238
294
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 ;
242
297
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 ( ) ) ;
246
299
} ,
247
300
} ;
248
301
0 commit comments