指令的使用
- 先来看下指令在Vue3中如何使用的,如果使用都不会,也就没有必要了解它是在Vue3中如何实现的了。
- 来看下这个例子,通过这个例子来分析它在Vue3中如何工作的。(该例子代码原地址 )
it('should work', async () => {
const count = ref(0)
function assertBindings(binding: DirectiveBinding) {
expect(binding.value).toBe(count.value)
expect(binding.arg).toBe('foo')
expect(binding.instance).toBe(_instance && _instance.proxy)
expect(binding.modifiers && binding.modifiers.ok).toBe(true)
}
const beforeMount = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
// should not be inserted yet
expect(el.parentNode).toBe(null)
expect(root.children.length).toBe(0)
assertBindings(binding)
expect(vnode).toBe(_vnode)
expect(prevVNode).toBe(null)
}) as DirectiveHook)
const mounted = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
// should be inserted now
expect(el.parentNode).toBe(root)
expect(root.children[0]).toBe(el)
assertBindings(binding)
expect(vnode).toBe(_vnode)
expect(prevVNode).toBe(null)
}) as DirectiveHook)
const beforeUpdate = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
expect(el.parentNode).toBe(root)
expect(root.children[0]).toBe(el)
// node should not have been updated yet
expect(el.children[0].text).toBe(`${count.value - 1}`)
assertBindings(binding)
expect(vnode).toBe(_vnode)
expect(prevVNode).toBe(_prevVnode)
}) as DirectiveHook)
const updated = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
expect(el.parentNode).toBe(root)
expect(root.children[0]).toBe(el)
// node should have been updated
expect(el.children[0].text).toBe(`${count.value}`)
assertBindings(binding)
expect(vnode).toBe(_vnode)
expect(prevVNode).toBe(_prevVnode)
}) as DirectiveHook)
const beforeUnmount = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
// should be removed now
expect(el.parentNode).toBe(root)
expect(root.children[0]).toBe(el)
assertBindings(binding)
expect(vnode).toBe(_vnode)
expect(prevVNode).toBe(null)
}) as DirectiveHook)
const unmounted = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
// should have been removed
expect(el.parentNode).toBe(null)
expect(root.children.length).toBe(0)
assertBindings(binding)
expect(vnode).toBe(_vnode)
expect(prevVNode).toBe(null)
}) as DirectiveHook)
const dir = {
beforeMount,
mounted,
beforeUpdate,
updated,
beforeUnmount,
unmounted
}
let _instance: ComponentInternalInstance | null = null
let _vnode: VNode | null = null
let _prevVnode: VNode | null = null
const Comp = {
setup() {
_instance = currentInstance
},
render() {
_prevVnode = _vnode
_vnode = withDirectives(h('div', count.value), [
[
dir,
// value
count.value,
// argument
'foo',
// modifiers
{ ok: true }
]
])
return _vnode
}
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(beforeMount).toHaveBeenCalledTimes(1)
expect(mounted).toHaveBeenCalledTimes(1)
count.value++
await nextTick()
expect(beforeUpdate).toHaveBeenCalledTimes(1)
expect(updated).toHaveBeenCalledTimes(1)
render(null, root)
expect(beforeUnmount).toHaveBeenCalledTimes(1)
expect(unmounted).toHaveBeenCalledTimes(1)
})
单侧的解读
我们平时一般会使用v-xxx:foo.ok="count",这个例子咋这样,看不懂,放弃?
别,别啊,这么快放弃,指令内部实现永远就搞不清楚了哦。
解读单侧
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(beforeMount).toHaveBeenCalledTimes(1)
expect(mounted).toHaveBeenCalledTimes(1)
count.value++
await nextTick()
expect(beforeUpdate).toHaveBeenCalledTimes(1)
expect(updated).toHaveBeenCalledTimes(1)
render(null, root)
expect(beforeUnmount).toHaveBeenCalledTimes(1)
expect(unmounted).toHaveBeenCalledTimes(1)
- 把h(Comp)创建好的vnode通过render函数挂载到root元素下,在元素安装之前和安装之后分别执行了beforeMount和mounted钩子,并且给了4个参数,供我们开发者使用
- 通过count.value++ 改变响应式值后,触发Comp组件更新,会调用beforeUpdate和updated钩子
- 通过render(null, root) 来卸载Comp组件,会调用beforeUnmount和unmounted钩子 这确实是我们想要的使用姿势和结果
疑问解答继续
上面的解释说明了这个单侧的意图,但是应该会有和我一样有疑问的同学。
-
withDirectives方法 是tm什么鬼?
-
说好的v-xxx:foo.ok="count"这个使用姿势呢?
-
难道指令不需要注册吗?
-
上面的钩子执行的时机在源码内部究竟如何实现的呢?
等等,大哥,别急,我先上完厕所,之后大概解释下,等讲内部实现时,这些问题都会揭开的.- 首选指令使用前肯定要注册
const app = Vue.createApp(App); app.directive('指令名', Object/Function)
这个帖子不讲注册的内部实现,所以各位同学瞅瞅就好
- v-xxx:foo.ok="count"这种姿势一般我们在模板中会这么写,那把这个例子改写下
const Comp = { directives: { xxx:dir }, setup() { _instance = currentInstance }, template: ` <div v-xxx:foo.ok="count"></div> ` }
我靠,熟悉的味道来了(template,真香定律)。
- withDirectives 方法
- 我们知道vue有模板编译模块和runtime-core和runtime-dom模块还有其他模块
- 其实该单侧省略了模板编译,直接进入了render函数编写,所以有的同学看起来就难受了
export function withDirectives<T extends VNode>( vnode: T, directives: DirectiveArguments ): T { const internalInstance = currentRenderingInstance if (internalInstance === null) { __DEV__ && warn(`withDirectives can only be used inside render functions.`) return vnode } const instance = internalInstance.proxy 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)) { dir = { mounted: dir, updated: dir } as ObjectDirective } bindings.push({ dir, instance, value, oldValue: void 0, arg, modifiers }) } return vnode }
- withDirectives(vNode, [ [dir, value, argument, modifiers] ])函数接受2个参数
第二个参数数组里面的成员,成员数组的第一个dir值必须为一个对象(虽然可以是函数,但是内部实现中,还是会转成对象){ beforeMount, mounted, beforeUpdate, updated, beforeUnmount, unmounted },值为钩子函数。
该函数主要给我们的vnode的dirs属性添加值。
目前为止,该单侧的解释基本说完了,终于终于终于可以讲解指令在内部如何实现的了。
内部实现
上面的单侧告诉我们,在元素安装 更新 卸载的时候分别调用了beforeMount, mounted, beforeUpdate, updated, beforeUnmount, unmounted 这几个钩子。要想从茫茫源码中找出内部何时调用这几个钩子函数的时机(熟练使用debugger的老司机可以忽略),不妨先思考下面几个问题:
- 该单侧通过render(h(Comp), root)来安装组件且挂载到root dom下, 那整个初始化的过程肯定先初始化Comp组件, 然后再安装Comp组件的模板?
- 模板安装过程中, 会根据subTree vnode 来安装子vnode中的el dom元素, vnode的安装的过程中会先执行created和beforeMount钩子, 安装完毕后在执行mounted钩子?
- 组件更新时, 会根据nextTree vnode 和 preTree vnode 进行patch, 在更新的过程中执行beforeUpdate和updated钩子?
- 组件卸载时, 在卸载的时候执行beforeUnmount和unmounted钩子?
- 那是不是分别在 安装 更新 卸载 这几个方法中,分别调用了这些钩子呢?
- 可以带着以上的疑问去看源码
初始化
- 先看下render函数及入口代码
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
container._vnode = vnode
}
render(h(Comp), root)
h(Comp)得到一个vnode节点,通过render函数把Comp组件安装到root dom下,Comp vnode节点描述如下
- 组件的安装、更新还是元素的安装、更新都会执行patch方法
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
optimized = false
) => {
// ...代码省略
switch (type) {
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
}
// ...代码省略
}
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
updateComponent(n1, n2, optimized)
}
}
当前vnode节点是组件,所以我们关心processComponent函数,该函数会处理组件的安装和组件的更新,因为当前处于安装时,所以关注下mountComponent函数
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 创建组件实例 instance
// setupComponent(instance) 处理组件的setup逻辑及拿到render函数
// 给instance实例添加update属性,安装和更新都会执行这个update方法
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
组件安装(更新也是同一个方法update函数),先看下setupRenderEffect 函数
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// create reactive effect for rendering
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// beforeMount hook
// onVnodeBeforeMount
// 拿到组件模板中的根subTree vnode节点(这里div vnode节点的dirs有值,接下来就看他啥时候被调用,可以看下图长得啥样)
// 安装 subTree 节点到 container dom 下
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
// mounted hook
// onVnodeMounted
} else {
// updateComponent 更新组件,这里先省略
}
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
}
subTree vnode 节点描述
安装组件的过程中, 先拿到模板的根vnode节点, 然后在调用patch方法进行安装根vnode. 接着调用patch方法会执行processComponent(因为上图的vnode是普通元素节点, type 为 div)
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}
可以看到,元素的安装和更新都在processElement函数中,那具有指令的vnode,是不是就在mountElement函数中执行呢?有点迫不及待了
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
// 根据vnode 调用hostCreateElement函数来创建真实的dom元素
// 如果vnode的孩子是文本节点则调用hostSetElementText来插入,否则就调用mountChildren方法来安装vnode的孩子
if (dirs) { // 如果vnode有指令,则先执行created钩子
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// 处理props
// 处理 scopeId
if (dirs) { // 如果vnode有指令,则执行beforeMount钩子
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}
// hostInsert(el, container, anchor) 把创建好的真实dom元素挂载到container dom下
if (
(vnodeHook = props && props.onVnodeMounted) ||
needCallTransitionHooks ||
dirs
) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
needCallTransitionHooks && transition!.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') // 等到本次事件循环结束后执行 指令的 mounted钩子
}, parentSuspense)
}
}
可以看到, 原来指令的created beforeMount mounted钩子执行时机是在mountElement 安装元素的时候 挨个执行的,指令还可以使用created钩子,单侧没有写.
这也证实了一开始的想法没有错误.
组件更新
-
count.value++ 通过改变count的值,来达到Comp组件更新(至于为什么更新,我后续可以单独开一个帖子讲解ref和effect)
-
组件的更新时会执行 instance.update方法
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// create reactive effect for rendering
instance.update = effect(function componentEffect() {
if(!instance.isMounted) {
// 组件安装 代码省略
} else {
// beforeUpdate hook
// onVnodeBeforeUpdate
// 调用renderComponentRoot方法重新获取新的根 vnode节点
const nextTree = renderComponentRoot(instance)
if (__DEV__) {
endMeasure(instance, `render`)
}
const prevTree = instance.subTree
instance.subTree = nextTree
// 根据新老vnode节点来更新
patch(
prevTree,
nextTree,
// parent may have changed if it's in a teleport
hostParentNode(prevTree.el!)!,
// anchor may have changed if it's in a fragment
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
// updated hook
// onVnodeUpdated
}
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
}
最终还是调用patch方法 -> processElement方法 -> patchElement方法, patch和processElement方法已经说过了,接下来看下patchElement方法
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
// invokeVNodeHook
if (dirs) { // vnode有dirs,单侧也提供了beforeUpdate钩子,所有会执行
invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
}
// patchProps
// patchChildren
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated') // 本次事件循环结束后执行 updated钩子
}, parentSuspense)
}
}
可以看出,指令的 beforeUpdate updated 钩子是在 patchElement方法中被调用的.
组件卸载
- 通过 render(null, root) 来卸载组件
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
container._vnode = vnode
}
组件安装完毕后,会在container dom下添加_vnode属性,现在调用render(null, root),就会执行unmount(container._vnode, null, null, true), 来看下unmount函数
const unmount: UnmountFn = (
vnode,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false
) => {
// unset ref
// 用这个变量来判断是否调用是否钩子
const shouldInvokeDirs = shapeFlag & ShapeFlags.ELEMENT && dirs
// invokeVNodeHook
if (shapeFlag & ShapeFlags.COMPONENT) {
unmountComponent(vnode.component!, parentSuspense, doRemove) // 如果vnode是组件vnode,则会调用unmountComponent方法,最终还是会执行unmount方法的
} else {
if (shouldInvokeDirs) { // 调用beforeUnmount钩子
invokeDirectiveHook(vnode, null, parentComponent, 'beforeUnmount')
}
// unmountChildren
if (doRemove) { // 把vnode中el元素从父节点上删除
remove(vnode)
}
if ((vnodeHook = props && props.onVnodeUnmounted) || shouldInvokeDirs) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
shouldInvokeDirs &&
invokeDirectiveHook(vnode, null, parentComponent, 'unmounted') // 执行 unmounted 钩子
}, parentSuspense)
}
}
}
可以看出,组件卸载时在 unmount 方法中会执行 beforeUnmount 和 unmounted钩子
unmount方法适用于卸载任意类型的vnode节点,对于本次的单测,其中的if else 分支都走到了, 调用unmountComponent方法内部还是会调用unmount方法来卸载
subTree vnode,所以会执行到 beforeUnmount和unmounted钩子.
总结
对于指令在普通vnode上:
初始化时:
在安装元素执行mountElement方法时,会调用 created beforeMount mounted 钩子
更新时:
在更新元素执行patchElement方法时,会调用 beforeUpdate updated 钩子
卸载时:
在卸载元素执行unmount方法时,会调用 beforeUnmount unmounted 钩子