From 5d65a83f2efb6cddf619d806f7b5c1a21c4cb32c Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Mon, 8 Jan 2024 01:19:23 +0800 Subject: [PATCH 01/10] feat(runtime): lifecycle beforeUpdate and Updated hooks --- packages/runtime-vapor/src/component.ts | 2 + packages/runtime-vapor/src/directive.ts | 23 +++--- packages/runtime-vapor/src/renderWatch.ts | 89 +++++++++++++++++++++-- packages/runtime-vapor/src/scheduler.ts | 26 ++++--- playground/src/App.vue | 16 +++- 5 files changed, 132 insertions(+), 24 deletions(-) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index d1e5723f2..a181e7a47 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -47,6 +47,7 @@ export interface ComponentInternalInstance { // lifecycle get isMounted(): boolean get isUnmounted(): boolean + isUpdating: boolean isUnmountedRef: Ref isMountedRef: Ref // TODO: registory of provides, lifecycles, ... @@ -155,6 +156,7 @@ export const createComponentInstance = ( get isUnmounted() { return isUnmountedRef.value }, + isUpdating: false, isMountedRef, isUnmountedRef, // TODO: registory of provides, appContext, lifecycles, ... diff --git a/packages/runtime-vapor/src/directive.ts b/packages/runtime-vapor/src/directive.ts index f59ce6a14..a13f461a2 100644 --- a/packages/runtime-vapor/src/directive.ts +++ b/packages/runtime-vapor/src/directive.ts @@ -1,6 +1,8 @@ import { isFunction } from '@vue/shared' import { type ComponentInternalInstance, currentInstance } from './component' import { watchEffect } from './apiWatch' +import { pauseTracking, resetTracking } from '@vue/reactivity' +import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling' export type DirectiveModifiers = Record @@ -27,7 +29,7 @@ export type DirectiveHookName = | 'created' | 'beforeMount' | 'mounted' - // | 'beforeUpdate' + | 'beforeUpdate' | 'updated' | 'beforeUnmount' | 'unmounted' @@ -93,12 +95,7 @@ export function withDirectives( } bindings.push(binding) - callDirectiveHook(node, binding, 'created') - - watchEffect(() => { - if (!instance.isMountedRef.value) return - callDirectiveHook(node, binding, 'updated') - }) + callDirectiveHook(node, binding, instance, 'created') } return node @@ -114,7 +111,7 @@ export function invokeDirectiveHook( for (const node of nodes) { const directives = instance.dirs.get(node) || [] for (const binding of directives) { - callDirectiveHook(node, binding, name) + callDirectiveHook(node, binding, instance, name) } } } @@ -122,6 +119,7 @@ export function invokeDirectiveHook( function callDirectiveHook( node: Node, binding: DirectiveBinding, + instance: ComponentInternalInstance | null, name: DirectiveHookName, ) { const { dir } = binding @@ -133,5 +131,12 @@ function callDirectiveHook( binding.oldValue = binding.value binding.value = newValue - hook(node, binding) + // disable tracking inside all lifecycle hooks + // since they can potentially be called inside effects. + pauseTracking() + callWithAsyncErrorHandling(hook, instance, VaporErrorCodes.DIRECTIVE_HOOK, [ + node, + binding, + ]) + resetTracking() } diff --git a/packages/runtime-vapor/src/renderWatch.ts b/packages/runtime-vapor/src/renderWatch.ts index fd9385fc5..c7ca26fad 100644 --- a/packages/runtime-vapor/src/renderWatch.ts +++ b/packages/runtime-vapor/src/renderWatch.ts @@ -1,14 +1,21 @@ import { - type BaseWatchErrorCodes, + BaseWatchErrorCodes, type BaseWatchOptions, baseWatch, getCurrentScope, } from '@vue/reactivity' -import { NOOP, remove } from '@vue/shared' +import { NOOP, invokeArrayFns, remove } from '@vue/shared' import { currentInstance } from './component' -import { createVaporRenderingScheduler } from './scheduler' -import { handleError as handleErrorWithInstance } from './errorHandling' +import { + createVaporRenderingScheduler, + queuePostRenderEffect, +} from './scheduler' +import { + callWithAsyncErrorHandling, + handleError as handleErrorWithInstance, +} from './errorHandling' import { warn } from './warning' +import { invokeDirectiveHook } from './directive' type WatchStopHandle = () => void @@ -33,11 +40,31 @@ function doWatch(source: any, cb?: any): WatchStopHandle { // TODO: SSR // if (__SSR__) {} + if (__DEV__ && !currentInstance) { + warn( + `${cb ? 'renderWatch' : 'renderEffect'}()` + + ' is an internal API and it can only be used inside render()', + ) + } + + if (cb) { + // watch + cb = wrapEffectCallback(cb) + } else { + // effect + source = wrapEffectCallback(source) + } + const instance = getCurrentScope() === currentInstance?.scope ? currentInstance : null - extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) => + extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) => { + // callback error handling is in wrapEffectCallback + if (type === BaseWatchErrorCodes.WATCH_CALLBACK) { + throw err + } handleErrorWithInstance(err, instance, type) + } extendOptions.scheduler = createVaporRenderingScheduler(instance) let effect = baseWatch(source, cb, extendOptions) @@ -53,3 +80,55 @@ function doWatch(source: any, cb?: any): WatchStopHandle { return unwatch } + +function wrapEffectCallback(callback: (...args: any[]) => any): Function { + const instance = currentInstance! + + return (...args: any[]) => { + // with lifecycle + if (instance.isMounted) { + const { bu, u, dirs } = instance + // currentInstance.updating = true + // beforeUpdate hook + const isFirstEffect = !instance.isUpdating + if (isFirstEffect) { + if (bu) { + invokeArrayFns(bu) + } + if (dirs) { + invokeDirectiveHook(instance, 'beforeUpdate') + } + instance.isUpdating = true + } + + // run callback + callWithAsyncErrorHandling( + callback, + instance, + BaseWatchErrorCodes.WATCH_CALLBACK, + args, + ) + + if (isFirstEffect) { + if (dirs) { + queuePostRenderEffect(() => { + instance.isUpdating = false + invokeDirectiveHook(instance, 'updated') + }) + } + // updated hook + if (u) { + queuePostRenderEffect(u) + } + } + } else { + // is not mounted + callWithAsyncErrorHandling( + callback, + instance, + BaseWatchErrorCodes.WATCH_CALLBACK, + args, + ) + } + } +} diff --git a/packages/runtime-vapor/src/scheduler.ts b/packages/runtime-vapor/src/scheduler.ts index 2be470254..7a5afb011 100644 --- a/packages/runtime-vapor/src/scheduler.ts +++ b/packages/runtime-vapor/src/scheduler.ts @@ -1,5 +1,6 @@ import type { Scheduler } from '@vue/reactivity' import type { ComponentInternalInstance } from './component' +import { isArray } from '@vue/shared' export interface SchedulerJob extends Function { id?: number @@ -73,15 +74,22 @@ function queueJob(job: SchedulerJob) { } } -export function queuePostRenderEffect(cb: SchedulerJob) { - if ( - !activePostFlushCbs || - !activePostFlushCbs.includes( - cb, - cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex, - ) - ) { - pendingPostFlushCbs.push(cb) +export function queuePostRenderEffect(cb: SchedulerJobs) { + if (!isArray(cb)) { + if ( + !activePostFlushCbs || + !activePostFlushCbs.includes( + cb, + cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex, + ) + ) { + pendingPostFlushCbs.push(cb) + } + } else { + // if cb is an array, it is a component lifecycle hook which can only be + // triggered by a job, which is already deduped in the main queue, so + // we can skip duplicate check here to improve perf + pendingPostFlushCbs.push(...cb) } queueFlush() } diff --git a/playground/src/App.vue b/playground/src/App.vue index f7c7a681d..070e727a6 100644 --- a/playground/src/App.vue +++ b/playground/src/App.vue @@ -5,6 +5,8 @@ import { onMounted, onBeforeMount, getCurrentInstance, + onBeforeUpdate, + onUpdated, } from 'vue/vapor' const instance = getCurrentInstance()! @@ -26,12 +28,24 @@ onMounted(() => { count.value++ }, 1000) }) + +onBeforeUpdate(() => { + console.log('before updated') +}) +onUpdated(() => { + console.log('updated') +}) + +const log = (arg: any) => { + console.log('callback in render effect') + return arg +}