From 1106e2208dfab9c1e33be216b058ee7743be21d7 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 25 Sep 2018 13:39:19 -0400 Subject: [PATCH] feat: vnode hooks --- packages/core/src/componentProps.ts | 9 ++--- packages/core/src/createRenderer.ts | 56 +++++++++++++++++++++++------ packages/core/src/utils.ts | 4 ++- packages/core/src/vdom.ts | 5 +-- 4 files changed, 57 insertions(+), 17 deletions(-) diff --git a/packages/core/src/componentProps.ts b/packages/core/src/componentProps.ts index 375a7155..6686fb91 100644 --- a/packages/core/src/componentProps.ts +++ b/packages/core/src/componentProps.ts @@ -1,4 +1,4 @@ -import { EMPTY_OBJ, nativeOnRE } from './utils' +import { EMPTY_OBJ, nativeOnRE, vnodeHookRE } from './utils' import { Component, ComponentClass, @@ -92,14 +92,15 @@ export function resolveProps( if (key === 'key' || key === 'ref' || key === 'slots') { continue } - // class, style & nativeOn are always extracted into a separate `attrs` - // object, which can then be merged onto child component root. - // in addition, if the component has explicitly declared props, then + // class, style, nativeOn & directive hooks are always extracted into a + // separate `attrs` object, which can then be merged onto child component + // root. in addition, if the component has explicitly declared props, then // any non-matching props are extracted into `attrs` as well. let isNativeOn if ( key === 'class' || key === 'style' || + vnodeHookRE.test(key) || (isNativeOn = nativeOnRE.test(key)) || (hasDeclaredProps && !options.hasOwnProperty(key)) ) { diff --git a/packages/core/src/createRenderer.ts b/packages/core/src/createRenderer.ts index cf27d189..b6ff8dfd 100644 --- a/packages/core/src/createRenderer.ts +++ b/packages/core/src/createRenderer.ts @@ -156,13 +156,14 @@ export function createRenderer(options: RendererOptions) { } } - // lifecycle hooks ----------------------------------------------------------- + // lifecycle lifecycleHooks ----------------------------------------------------------- - const hooks: Function[] = [] + const lifecycleHooks: Function[] = [] + const vnodeUpdatedHooks: Function[] = [] function flushHooks() { let fn - while ((fn = hooks.shift())) { + while ((fn = lifecycleHooks.shift())) { fn() } } @@ -222,6 +223,9 @@ export function createRenderer(options: RendererOptions) { for (const key in data) { patchData(el, key, null, data[key], null, vnode, isSVG) } + if (data.vnodeBeforeMount) { + data.vnodeBeforeMount(vnode) + } } if (childFlags !== ChildrenFlags.NO_CHILDREN) { const hasSVGChildren = isSVG && tag !== 'foreignObject' @@ -243,11 +247,16 @@ export function createRenderer(options: RendererOptions) { if (ref) { mountRef(ref, el) } + if (data != null && data.vnodeMounted) { + lifecycleHooks.push(() => { + data.vnodeMounted(vnode) + }) + } return el } function mountRef(ref: Ref, el: RenderNode | MountedComponent) { - hooks.push(() => { + lifecycleHooks.push(() => { ref(el) }) } @@ -438,10 +447,14 @@ export function createRenderer(options: RendererOptions) { } const el = (nextVNode.el = prevVNode.el) as RenderNode - - // data const prevData = prevVNode.data const nextData = nextVNode.data + + if (nextData != null && nextData.vnodeBeforeUpdate) { + nextData.vnodeBeforeUpdate(nextVNode, prevVNode) + } + + // patch data if (prevData !== nextData) { const prevDataOrEmpty = prevData || EMPTY_OBJ const nextDataOrEmpty = nextData || EMPTY_OBJ @@ -483,6 +496,12 @@ export function createRenderer(options: RendererOptions) { isSVG && nextVNode.tag !== 'foreignObject', null ) + + if (nextData != null && nextData.vnodeUpdated) { + vnodeUpdatedHooks.push(() => { + nextData.vnodeUpdated(nextVNode, prevVNode) + }) + } } function patchComponent( @@ -1064,12 +1083,19 @@ export function createRenderer(options: RendererOptions) { // unmounting ---------------------------------------------------------------- function unmount(vnode: VNode) { - const { flags, children, childFlags, ref } = vnode - if (flags & VNodeFlags.ELEMENT || flags & VNodeFlags.FRAGMENT) { + const { flags, data, children, childFlags, ref } = vnode + const isElement = flags & VNodeFlags.ELEMENT + if (isElement || flags & VNodeFlags.FRAGMENT) { + if (isElement && data != null && data.vnodeBeforeUnmount) { + data.vnodeBeforeUnmount(vnode) + } unmountChildren(children as VNodeChildren, childFlags) if (teardownVNode !== void 0) { teardownVNode(vnode) } + if (isElement && data != null && data.vnodeUnmounted) { + data.vnodeUnmounted(vnode) + } } else if (flags & VNodeFlags.COMPONENT) { if (flags & VNodeFlags.COMPONENT_STATEFUL) { unmountComponentInstance(children as MountedComponent) @@ -1187,7 +1213,7 @@ export function createRenderer(options: RendererOptions) { mountRef(ref, instance) } if (instance.mounted) { - hooks.push(() => { + lifecycleHooks.push(() => { ;(instance as any).mounted.call(instance.$proxy) }) } @@ -1227,10 +1253,20 @@ export function createRenderer(options: RendererOptions) { // will be added to the queue AFTER the parent's, but they should be // invoked BEFORE the parent's. Therefore we add them to the head of the // queue instead. - hooks.unshift(() => { + lifecycleHooks.unshift(() => { ;(instance as any).updated.call(instance.$proxy, nextVNode) }) } + + if (vnodeUpdatedHooks.length > 0) { + const vnodeUpdatedHooksForCurrentInstance = vnodeUpdatedHooks.slice() + vnodeUpdatedHooks.length = 0 + lifecycleHooks.unshift(() => { + for (let i = 0; i < vnodeUpdatedHooksForCurrentInstance.length; i++) { + vnodeUpdatedHooksForCurrentInstance[i]() + } + }) + } } function unmountComponentInstance(instance: MountedComponent) { diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index e006fba8..60052e70 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -4,7 +4,9 @@ export const NOOP = () => {} export const onRE = /^on/ export const nativeOnRE = /^nativeOn/ -export const reservedPropRE = /^(?:key|ref|slots)$|^nativeOn/ +export const vnodeHookRE = /^vnode/ +export const handlersRE = /^on|^nativeOn|^vnode/ +export const reservedPropRE = /^(?:key|ref|slots)$|^nativeOn|^vnode/ export function normalizeStyle( value: any diff --git a/packages/core/src/vdom.ts b/packages/core/src/vdom.ts index c5ef61af..e85d1b31 100644 --- a/packages/core/src/vdom.ts +++ b/packages/core/src/vdom.ts @@ -5,7 +5,7 @@ import { } from './component' import { VNodeFlags, ChildrenFlags } from './flags' import { createComponentClassFromOptions } from './componentUtils' -import { normalizeClass, normalizeStyle, onRE, nativeOnRE } from './utils' +import { normalizeClass, normalizeStyle, handlersRE } from './utils' // Vue core is platform agnostic, so we are not using Element for "DOM" nodes. export interface RenderNode { @@ -270,7 +270,8 @@ export function cloneVNode(vnode: VNode, extraData?: VNodeData): VNode { clonedData.class = normalizeClass([clonedData.class, extraData.class]) } else if (key === 'style') { clonedData.style = normalizeStyle([clonedData.style, extraData.style]) - } else if (onRE.test(key) || nativeOnRE.test(key)) { + } else if (handlersRE.test(key)) { + // on*, nativeOn*, vnode* const existing = clonedData[key] clonedData[key] = existing ? [].concat(existing, extraData[key])