feat: vnode hooks

This commit is contained in:
Evan You 2018-09-25 13:39:19 -04:00
parent 8cafad1fd3
commit 1106e2208d
4 changed files with 57 additions and 17 deletions

View File

@ -1,4 +1,4 @@
import { EMPTY_OBJ, nativeOnRE } from './utils' import { EMPTY_OBJ, nativeOnRE, vnodeHookRE } from './utils'
import { import {
Component, Component,
ComponentClass, ComponentClass,
@ -92,14 +92,15 @@ export function resolveProps(
if (key === 'key' || key === 'ref' || key === 'slots') { if (key === 'key' || key === 'ref' || key === 'slots') {
continue continue
} }
// class, style & nativeOn are always extracted into a separate `attrs` // class, style, nativeOn & directive hooks are always extracted into a
// object, which can then be merged onto child component root. // separate `attrs` object, which can then be merged onto child component
// in addition, if the component has explicitly declared props, then // root. in addition, if the component has explicitly declared props, then
// any non-matching props are extracted into `attrs` as well. // any non-matching props are extracted into `attrs` as well.
let isNativeOn let isNativeOn
if ( if (
key === 'class' || key === 'class' ||
key === 'style' || key === 'style' ||
vnodeHookRE.test(key) ||
(isNativeOn = nativeOnRE.test(key)) || (isNativeOn = nativeOnRE.test(key)) ||
(hasDeclaredProps && !options.hasOwnProperty(key)) (hasDeclaredProps && !options.hasOwnProperty(key))
) { ) {

View File

@ -156,13 +156,14 @@ export function createRenderer(options: RendererOptions) {
} }
} }
// lifecycle hooks ----------------------------------------------------------- // lifecycle lifecycleHooks -----------------------------------------------------------
const hooks: Function[] = [] const lifecycleHooks: Function[] = []
const vnodeUpdatedHooks: Function[] = []
function flushHooks() { function flushHooks() {
let fn let fn
while ((fn = hooks.shift())) { while ((fn = lifecycleHooks.shift())) {
fn() fn()
} }
} }
@ -222,6 +223,9 @@ export function createRenderer(options: RendererOptions) {
for (const key in data) { for (const key in data) {
patchData(el, key, null, data[key], null, vnode, isSVG) patchData(el, key, null, data[key], null, vnode, isSVG)
} }
if (data.vnodeBeforeMount) {
data.vnodeBeforeMount(vnode)
}
} }
if (childFlags !== ChildrenFlags.NO_CHILDREN) { if (childFlags !== ChildrenFlags.NO_CHILDREN) {
const hasSVGChildren = isSVG && tag !== 'foreignObject' const hasSVGChildren = isSVG && tag !== 'foreignObject'
@ -243,11 +247,16 @@ export function createRenderer(options: RendererOptions) {
if (ref) { if (ref) {
mountRef(ref, el) mountRef(ref, el)
} }
if (data != null && data.vnodeMounted) {
lifecycleHooks.push(() => {
data.vnodeMounted(vnode)
})
}
return el return el
} }
function mountRef(ref: Ref, el: RenderNode | MountedComponent) { function mountRef(ref: Ref, el: RenderNode | MountedComponent) {
hooks.push(() => { lifecycleHooks.push(() => {
ref(el) ref(el)
}) })
} }
@ -438,10 +447,14 @@ export function createRenderer(options: RendererOptions) {
} }
const el = (nextVNode.el = prevVNode.el) as RenderNode const el = (nextVNode.el = prevVNode.el) as RenderNode
// data
const prevData = prevVNode.data const prevData = prevVNode.data
const nextData = nextVNode.data const nextData = nextVNode.data
if (nextData != null && nextData.vnodeBeforeUpdate) {
nextData.vnodeBeforeUpdate(nextVNode, prevVNode)
}
// patch data
if (prevData !== nextData) { if (prevData !== nextData) {
const prevDataOrEmpty = prevData || EMPTY_OBJ const prevDataOrEmpty = prevData || EMPTY_OBJ
const nextDataOrEmpty = nextData || EMPTY_OBJ const nextDataOrEmpty = nextData || EMPTY_OBJ
@ -483,6 +496,12 @@ export function createRenderer(options: RendererOptions) {
isSVG && nextVNode.tag !== 'foreignObject', isSVG && nextVNode.tag !== 'foreignObject',
null null
) )
if (nextData != null && nextData.vnodeUpdated) {
vnodeUpdatedHooks.push(() => {
nextData.vnodeUpdated(nextVNode, prevVNode)
})
}
} }
function patchComponent( function patchComponent(
@ -1064,12 +1083,19 @@ export function createRenderer(options: RendererOptions) {
// unmounting ---------------------------------------------------------------- // unmounting ----------------------------------------------------------------
function unmount(vnode: VNode) { function unmount(vnode: VNode) {
const { flags, children, childFlags, ref } = vnode const { flags, data, children, childFlags, ref } = vnode
if (flags & VNodeFlags.ELEMENT || flags & VNodeFlags.FRAGMENT) { const isElement = flags & VNodeFlags.ELEMENT
if (isElement || flags & VNodeFlags.FRAGMENT) {
if (isElement && data != null && data.vnodeBeforeUnmount) {
data.vnodeBeforeUnmount(vnode)
}
unmountChildren(children as VNodeChildren, childFlags) unmountChildren(children as VNodeChildren, childFlags)
if (teardownVNode !== void 0) { if (teardownVNode !== void 0) {
teardownVNode(vnode) teardownVNode(vnode)
} }
if (isElement && data != null && data.vnodeUnmounted) {
data.vnodeUnmounted(vnode)
}
} else if (flags & VNodeFlags.COMPONENT) { } else if (flags & VNodeFlags.COMPONENT) {
if (flags & VNodeFlags.COMPONENT_STATEFUL) { if (flags & VNodeFlags.COMPONENT_STATEFUL) {
unmountComponentInstance(children as MountedComponent) unmountComponentInstance(children as MountedComponent)
@ -1187,7 +1213,7 @@ export function createRenderer(options: RendererOptions) {
mountRef(ref, instance) mountRef(ref, instance)
} }
if (instance.mounted) { if (instance.mounted) {
hooks.push(() => { lifecycleHooks.push(() => {
;(instance as any).mounted.call(instance.$proxy) ;(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 // 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 // invoked BEFORE the parent's. Therefore we add them to the head of the
// queue instead. // queue instead.
hooks.unshift(() => { lifecycleHooks.unshift(() => {
;(instance as any).updated.call(instance.$proxy, nextVNode) ;(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) { function unmountComponentInstance(instance: MountedComponent) {

View File

@ -4,7 +4,9 @@ export const NOOP = () => {}
export const onRE = /^on/ export const onRE = /^on/
export const nativeOnRE = /^nativeOn/ 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( export function normalizeStyle(
value: any value: any

View File

@ -5,7 +5,7 @@ import {
} from './component' } from './component'
import { VNodeFlags, ChildrenFlags } from './flags' import { VNodeFlags, ChildrenFlags } from './flags'
import { createComponentClassFromOptions } from './componentUtils' 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. // Vue core is platform agnostic, so we are not using Element for "DOM" nodes.
export interface RenderNode { export interface RenderNode {
@ -270,7 +270,8 @@ export function cloneVNode(vnode: VNode, extraData?: VNodeData): VNode {
clonedData.class = normalizeClass([clonedData.class, extraData.class]) clonedData.class = normalizeClass([clonedData.class, extraData.class])
} else if (key === 'style') { } else if (key === 'style') {
clonedData.style = normalizeStyle([clonedData.style, extraData.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] const existing = clonedData[key]
clonedData[key] = existing clonedData[key] = existing
? [].concat(existing, extraData[key]) ? [].concat(existing, extraData[key])