diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 470d4ec4d..3dbfd0d4d 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -131,7 +131,6 @@ export interface ComponentInternalInstance { data: Data props: Data attrs: Data - vnodeHooks: Data slots: Slots proxy: ComponentPublicInstance | null // alternative proxy used only for runtime-compiled render functions using @@ -204,7 +203,6 @@ export function createComponentInstance( data: EMPTY_OBJ, props: EMPTY_OBJ, attrs: EMPTY_OBJ, - vnodeHooks: EMPTY_OBJ, slots: EMPTY_OBJ, refs: EMPTY_OBJ, diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index c243d5179..d2f35da2e 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -106,7 +106,6 @@ export function resolveProps( const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)! const props: Data = {} let attrs: Data | undefined = undefined - let vnodeHooks: Data | undefined = undefined // update the instance propsProxy (passed to setup()) to trigger potential // changes @@ -128,10 +127,6 @@ export function resolveProps( const value = rawProps[key] // key, ref are reserved and never passed down if (isReservedProp(key)) { - if (key !== 'key' && key !== 'ref') { - // vnode hooks. - ;(vnodeHooks || (vnodeHooks = {}))[key] = value - } continue } // prop option names are camelized during normalization, so to support @@ -208,7 +203,6 @@ export function resolveProps( instance.props = props instance.attrs = attrs || EMPTY_OBJ - instance.vnodeHooks = vnodeHooks || EMPTY_OBJ } const normalizationMap = new WeakMap< diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index 523565929..316bb80ac 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -45,7 +45,6 @@ export function renderComponentRoot( props, slots, attrs, - vnodeHooks, emit, renderCache } = instance @@ -104,10 +103,6 @@ export function renderComponentRoot( } } - // inherit vnode hooks - if (vnodeHooks !== EMPTY_OBJ) { - result = cloneVNode(result, vnodeHooks) - } // inherit scopeId const parentScopeId = parent && parent.type.__scopeId if (parentScopeId) { diff --git a/packages/runtime-core/src/directives.ts b/packages/runtime-core/src/directives.ts index eb897c355..434ab83f8 100644 --- a/packages/runtime-core/src/directives.ts +++ b/packages/runtime-core/src/directives.ts @@ -11,8 +11,8 @@ return withDirectives(h(comp), [ ]) */ -import { VNode, VNodeHook } from './vnode' -import { isFunction, EMPTY_OBJ, makeMap, EMPTY_ARR } from '@vue/shared' +import { VNode } from './vnode' +import { isFunction, EMPTY_OBJ, makeMap } from '@vue/shared' import { warn } from './warning' import { ComponentInternalInstance, Data } from './component' import { currentRenderingInstance } from './componentRenderUtils' @@ -72,36 +72,6 @@ export function validateDirectiveName(name: string) { } } -const directiveToVnodeHooksMap = /*#__PURE__*/ [ - 'beforeMount', - 'mounted', - 'beforeUpdate', - 'updated', - 'beforeUnmount', - 'unmounted' -].reduce( - (map, key: keyof ObjectDirective) => { - const vnodeKey = `onVnode` + key[0].toUpperCase() + key.slice(1) - const vnodeHook = (vnode: VNode, prevVnode: VNode | null) => { - const bindings = vnode.dirs! - const prevBindings = prevVnode ? prevVnode.dirs! : EMPTY_ARR - for (let i = 0; i < bindings.length; i++) { - const binding = bindings[i] - const hook = binding.dir[key] as DirectiveHook - if (hook != null) { - if (prevVnode != null) { - binding.oldValue = prevBindings[i].value - } - hook(vnode.el, binding, vnode, prevVnode) - } - } - } - map[key] = [vnodeKey, vnodeHook] - return map - }, - {} as Record -) - // Directive, value, argument, modifiers export type DirectiveArguments = Array< | [Directive] @@ -120,9 +90,7 @@ export function withDirectives( return vnode } const instance = internalInstance.proxy - const props = vnode.props || (vnode.props = {}) - const bindings = vnode.dirs || (vnode.dirs = new Array(directives.length)) - const injected: Record = {} + const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = []) for (let i = 0; i < directives.length; i++) { let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i] if (isFunction(dir)) { @@ -131,36 +99,39 @@ export function withDirectives( updated: dir } as ObjectDirective } - bindings[i] = { + bindings.push({ dir, instance, value, oldValue: void 0, arg, modifiers - } - // inject onVnodeXXX hooks - for (const key in dir) { - const mapped = directiveToVnodeHooksMap[key] - if (mapped && !injected[key]) { - const { 0: hookName, 1: hook } = mapped - const existing = props[hookName] - props[hookName] = existing ? [].concat(existing, hook as any) : hook - injected[key] = true - } - } + }) } return vnode } export function invokeDirectiveHook( - hook: VNodeHook | VNodeHook[], - instance: ComponentInternalInstance | null, vnode: VNode, - prevVNode: VNode | null = null + prevVNode: VNode | null, + instance: ComponentInternalInstance | null, + name: keyof ObjectDirective ) { - callWithAsyncErrorHandling(hook, instance, ErrorCodes.DIRECTIVE_HOOK, [ - vnode, - prevVNode - ]) + const bindings = vnode.dirs! + const oldBindings = prevVNode && prevVNode.dirs! + for (let i = 0; i < bindings.length; i++) { + const binding = bindings[i] + if (oldBindings) { + binding.oldValue = oldBindings[i].value + } + const hook = binding.dir[name] as DirectiveHook | undefined + if (hook) { + callWithAsyncErrorHandling(hook, instance, ErrorCodes.DIRECTIVE_HOOK, [ + vnode.el, + binding, + vnode, + prevVNode + ]) + } + } } diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts index d9a0fc0b6..f027f0c3f 100644 --- a/packages/runtime-core/src/errorHandling.ts +++ b/packages/runtime-core/src/errorHandling.ts @@ -13,6 +13,7 @@ export const enum ErrorCodes { WATCH_CLEANUP, NATIVE_EVENT_HANDLER, COMPONENT_EVENT_HANDLER, + VNODE_HOOK, DIRECTIVE_HOOK, TRANSITION_HOOK, APP_ERROR_HANDLER, @@ -42,6 +43,7 @@ export const ErrorTypeStrings: Record = { [ErrorCodes.WATCH_CLEANUP]: 'watcher cleanup function', [ErrorCodes.NATIVE_EVENT_HANDLER]: 'native event handler', [ErrorCodes.COMPONENT_EVENT_HANDLER]: 'component event handler', + [ErrorCodes.VNODE_HOOK]: 'vnode hook', [ErrorCodes.DIRECTIVE_HOOK]: 'directive hook', [ErrorCodes.TRANSITION_HOOK]: 'transition hook', [ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler', diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 945fb8a30..d81e6abdf 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -1,4 +1,12 @@ -import { VNode, normalizeVNode, Text, Comment, Static, Fragment } from './vnode' +import { + VNode, + normalizeVNode, + Text, + Comment, + Static, + Fragment, + VNodeHook +} from './vnode' import { flushPostFlushCbs } from './scheduler' import { ComponentInternalInstance } from './component' import { invokeDirectiveHook } from './directives' @@ -10,7 +18,7 @@ import { isOn, isString } from '@vue/shared' -import { RendererInternals } from './renderer' +import { RendererInternals, invokeVNodeHook } from './renderer' import { SuspenseImpl, SuspenseBoundary, @@ -192,7 +200,7 @@ export function createHydrationFunctions( optimized: boolean ) => { optimized = optimized || vnode.dynamicChildren !== null - const { props, patchFlag, shapeFlag } = vnode + const { props, patchFlag, shapeFlag, dirs } = vnode // skip props & children if this is hoisted static nodes if (patchFlag !== PatchFlags.HOISTED) { // props @@ -212,16 +220,23 @@ export function createHydrationFunctions( // iterating through props. patchProp(el, 'onClick', null, props.onClick) } - // vnode hooks - const { onVnodeBeforeMount, onVnodeMounted } = props - if (onVnodeBeforeMount != null) { - invokeDirectiveHook(onVnodeBeforeMount, parentComponent, vnode) - } - if (onVnodeMounted != null) { - queueEffectWithSuspense(() => { - invokeDirectiveHook(onVnodeMounted, parentComponent, vnode) - }, parentSuspense) - } + } + // vnode / directive hooks + let vnodeHooks: VNodeHook | null | undefined + if ((vnodeHooks = props && props.onVnodeBeforeMount) != null) { + invokeVNodeHook(vnodeHooks, parentComponent, vnode) + } + if (dirs != null) { + invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount') + } + if ( + (vnodeHooks = props && props.onVnodeMounted) != null || + dirs != null + ) { + queueEffectWithSuspense(() => { + vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode) + dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') + }, parentSuspense) } // children if ( diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 7849b941b..7b25f23dd 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -51,7 +51,6 @@ import { import { resolveProps } from './componentProps' import { resolveSlots } from './componentSlots' import { pushWarningContext, popWarningContext, warn } from './warning' -import { invokeDirectiveHook } from './directives' import { ComponentPublicInstance } from './componentProxy' import { createAppAPI, CreateAppFunction } from './apiCreateApp' import { @@ -62,8 +61,13 @@ import { import { PortalImpl } from './components/Portal' import { KeepAliveSink, isKeepAlive } from './components/KeepAlive' import { registerHMR, unregisterHMR } from './hmr' -import { ErrorCodes, callWithErrorHandling } from './errorHandling' +import { + ErrorCodes, + callWithErrorHandling, + callWithAsyncErrorHandling +} from './errorHandling' import { createHydrationFunctions, RootHydrateFunction } from './hydration' +import { invokeDirectiveHook } from './directives' const __HMR__ = __BUNDLER__ && __DEV__ @@ -526,7 +530,15 @@ function baseCreateRenderer< ) => { let el: HostElement let vnodeHook: VNodeHook | undefined | null - const { type, props, shapeFlag, transition, scopeId, patchFlag } = vnode + const { + type, + props, + shapeFlag, + transition, + scopeId, + patchFlag, + dirs + } = vnode if ( vnode.el !== null && hostCloneNode !== undefined && @@ -546,9 +558,12 @@ function baseCreateRenderer< } } if ((vnodeHook = props.onVnodeBeforeMount) != null) { - invokeDirectiveHook(vnodeHook, parentComponent, vnode) + invokeVNodeHook(vnodeHook, parentComponent, vnode) } } + if (dirs != null) { + invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount') + } // scopeId if (__BUNDLER__) { @@ -585,11 +600,13 @@ function baseCreateRenderer< hostInsert(el, container, anchor) if ( (vnodeHook = props && props.onVnodeMounted) != null || - (transition != null && !transition.persisted) + (transition != null && !transition.persisted) || + dirs != null ) { queuePostRenderEffect(() => { - vnodeHook && invokeDirectiveHook(vnodeHook, parentComponent, vnode) + vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) transition && !transition.persisted && transition.enter(el) + dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') }, parentSuspense) } } @@ -630,13 +647,16 @@ function baseCreateRenderer< optimized: boolean ) => { const el = (n2.el = n1.el) as HostElement - let { patchFlag, dynamicChildren } = n2 + let { patchFlag, dynamicChildren, dirs } = n2 const oldProps = (n1 && n1.props) || EMPTY_OBJ const newProps = n2.props || EMPTY_OBJ let vnodeHook: VNodeHook | undefined | null if ((vnodeHook = newProps.onVnodeBeforeUpdate) != null) { - invokeDirectiveHook(vnodeHook, parentComponent, n2, n1) + invokeVNodeHook(vnodeHook, parentComponent, n2, n1) + } + if (dirs != null) { + invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate') } if (__HMR__ && parentComponent && parentComponent.renderUpdated) { @@ -750,9 +770,10 @@ function baseCreateRenderer< ) } - if ((vnodeHook = newProps.onVnodeUpdated) != null) { + if ((vnodeHook = newProps.onVnodeUpdated) != null || dirs != null) { queuePostRenderEffect(() => { - invokeDirectiveHook(vnodeHook!, parentComponent, n2, n1) + vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1) + dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated') }, parentSuspense) } } @@ -1063,12 +1084,19 @@ function baseCreateRenderer< // create reactive effect for rendering instance.update = effect(function componentEffect() { if (!instance.isMounted) { + let vnodeHook: VNodeHook | null | undefined + const { el, props } = initialVNode + const { bm, m, a, parent } = instance const subTree = (instance.subTree = renderComponentRoot(instance)) // beforeMount hook - if (instance.bm !== null) { - invokeHooks(instance.bm) + if (bm !== null) { + invokeHooks(bm) } - if (initialVNode.el && hydrateNode) { + // onVnodeBeforeMount + if ((vnodeHook = props && props.onVnodeBeforeMount) != null) { + invokeVNodeHook(vnodeHook, parent, initialVNode) + } + if (el && hydrateNode) { // vnode has adopted host node - perform hydration instead of mount. hydrateNode( initialVNode.el as Node, @@ -1089,36 +1117,50 @@ function baseCreateRenderer< initialVNode.el = subTree.el } // mounted hook - if (instance.m !== null) { - queuePostRenderEffect(instance.m, parentSuspense) + if (m !== null) { + queuePostRenderEffect(m, parentSuspense) + } + // onVnodeMounted + if ((vnodeHook = props && props.onVnodeMounted) != null) { + queuePostRenderEffect(() => { + invokeVNodeHook(vnodeHook!, parent, initialVNode) + }, parentSuspense) } // activated hook for keep-alive roots. if ( - instance.a !== null && - instance.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE + a !== null && + initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ) { - queuePostRenderEffect(instance.a, parentSuspense) + queuePostRenderEffect(a, parentSuspense) } instance.isMounted = true } else { // updateComponent // This is triggered by mutation of component's own state (next: null) // OR parent calling processComponent (next: HostVNode) - const { next } = instance - + let { next, bu, u, parent, vnode } = instance + let vnodeHook: VNodeHook | null | undefined if (__DEV__) { pushWarningContext(next || instance.vnode) } if (next !== null) { updateComponentPreRender(instance, next) + } else { + next = vnode } const nextTree = renderComponentRoot(instance) const prevTree = instance.subTree instance.subTree = nextTree // beforeUpdate hook - if (instance.bu !== null) { - invokeHooks(instance.bu) + if (bu !== null) { + invokeHooks(bu) + } + // onVnodeBeforeUpdate + if ( + (vnodeHook = next.props && next.props.onVnodeBeforeUpdate) != null + ) { + invokeVNodeHook(vnodeHook, parent, next, vnode) } // reset refs // only needed if previous patch had refs @@ -1136,7 +1178,7 @@ function baseCreateRenderer< parentSuspense, isSVG ) - instance.vnode.el = nextTree.el + next.el = nextTree.el if (next === null) { // self-triggered update. In case of HOC, update parent component // vnode el. HOC is indicated by parent instance's subTree pointing @@ -1144,10 +1186,15 @@ function baseCreateRenderer< updateHOCHostEl(instance, nextTree.el) } // updated hook - if (instance.u !== null) { - queuePostRenderEffect(instance.u, parentSuspense) + if (u !== null) { + queuePostRenderEffect(u, parentSuspense) + } + // onVnodeUpdated + if ((vnodeHook = next.props && next.props.onVnodeUpdated) != null) { + queuePostRenderEffect(() => { + invokeVNodeHook(vnodeHook!, parent, next!, vnode) + }, parentSuspense) } - if (__DEV__) { popWarningContext() } @@ -1617,7 +1664,8 @@ function baseCreateRenderer< parentSuspense, doRemove = false ) => { - const { props, ref, children, dynamicChildren, shapeFlag } = vnode + const { props, ref, children, dynamicChildren, shapeFlag, dirs } = vnode + const shouldInvokeDirs = dirs != null && shapeFlag & ShapeFlags.ELEMENT let vnodeHook: VNodeHook | undefined | null // unset ref @@ -1640,7 +1688,10 @@ function baseCreateRenderer< } if ((vnodeHook = props && props.onVnodeBeforeUnmount) != null) { - invokeDirectiveHook(vnodeHook, parentComponent, vnode) + invokeVNodeHook(vnodeHook, parentComponent, vnode) + } + if (shouldInvokeDirs) { + invokeDirectiveHook(vnode, null, parentComponent, 'beforeUnmount') } if (dynamicChildren != null) { @@ -1654,9 +1705,14 @@ function baseCreateRenderer< remove(vnode) } - if ((vnodeHook = props && props.onVnodeUnmounted) != null) { + if ( + (vnodeHook = props && props.onVnodeUnmounted) != null || + shouldInvokeDirs + ) { queuePostRenderEffect(() => { - invokeDirectiveHook(vnodeHook!, parentComponent, vnode) + vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) + shouldInvokeDirs && + invokeDirectiveHook(vnode, null, parentComponent, 'unmounted') }, parentSuspense) } } @@ -1880,6 +1936,18 @@ function baseCreateRenderer< } } +export function invokeVNodeHook( + hook: VNodeHook, + instance: ComponentInternalInstance | null, + vnode: VNode, + prevVNode: VNode | null = null +) { + callWithAsyncErrorHandling(hook, instance, ErrorCodes.VNODE_HOOK, [ + vnode, + prevVNode + ]) +} + // https://en.wikipedia.org/wiki/Longest_increasing_subsequence function getSequence(arr: number[]): number[] { const p = arr.slice() diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index ea0e1dbef..592aa8b33 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -61,7 +61,11 @@ export type VNodeNormalizedRef = [ComponentInternalInstance, VNodeRef] type VNodeMountHook = (vnode: VNode) => void type VNodeUpdateHook = (vnode: VNode, oldVNode: VNode) => void -export type VNodeHook = VNodeMountHook | VNodeUpdateHook +export type VNodeHook = + | VNodeMountHook + | VNodeUpdateHook + | VNodeMountHook[] + | VNodeUpdateHook[] export interface VNodeProps { [key: string]: any @@ -69,12 +73,12 @@ export interface VNodeProps { ref?: VNodeRef // vnode hooks - onVnodeBeforeMount?: VNodeMountHook - onVnodeMounted?: VNodeMountHook - onVnodeBeforeUpdate?: VNodeUpdateHook - onVnodeUpdated?: VNodeUpdateHook - onVnodeBeforeUnmount?: VNodeMountHook - onVnodeUnmounted?: VNodeMountHook + onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[] + onVnodeMounted?: VNodeMountHook | VNodeMountHook[] + onVnodeBeforeUpdate?: VNodeUpdateHook | VNodeUpdateHook[] + onVnodeUpdated?: VNodeUpdateHook | VNodeUpdateHook[] + onVnodeBeforeUnmount?: VNodeMountHook | VNodeMountHook[] + onVnodeUnmounted?: VNodeMountHook | VNodeMountHook[] } type VNodeChildAtom =