跳到主要内容

认识

2024年03月13日
柏拉文
越努力,越幸运

一、认识


Vue.js 3.0 指令本质上就是一个 JavaScript 对象,对象上挂着一些生命周期的钩子函数。这些钩子函数将来在不同的时期被调用执行,自定义指令跟 Vue3 底层内置的指令运行原理是一致的。

二、Render


如果特定标签元素使用了指令则在编译后的 render 函数中需要使用 withDirectives 函数进行处理相关指令,也就是往虚拟 DOM 中添加指令。

2.1 withDirectives

withDirectives 拥有两个参数,vnode 就是当前节点的虚拟 DOM 对象;directives 是一个由不同指令构成的数组,因为一个元素节点上可以应用多个指令;具体一个数组中的元素按顺序分别对应,dir 指令对象,value 指令对应的值,arg 参数,modifiers 修饰符(v-model.trim)。withDirectives 函数的核心功能就是给当前节点的 vnode 添加一个 dirs 属性,属性值就是这个元素所有应用的指令构成的对象数组。

export function withDirectives(
vnode,
directives
): T {
// 当前渲染的实例对象
const internalInstance = currentRenderingInstance
// 通过代理对象可以访问到 setup 的返回值、props 等
const instance = internalInstance.proxy
// 这个赋值很意思,充分利用了引用地址相同的原理
const bindings = vnode.dirs || (vnode.dirs = [])
for (let i = 0; i < directives.length; i++) {
// dir 指令对象,value 指令对应的值,arg 参数,modifiers 修饰符(v-model.trim)
let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
if (isFunction(dir)) {
dir = {
mounted: dir,
updated: dir
}
}
// 把指令对象绑定到 vnode.dirs 数组中
bindings.push({
dir,
instance,
value,
oldValue: void 0,
arg,
modifiers
})
}
return vnode
}

三、、Mount

render 函数中已经通过 withDirectives 函数将相关指令对象设置到了 vnodedirs 属性上。所以在元素 vnode 挂载的过程中又会从 vnodedirs 属性中取出来相关的指令对象进行执行。在 Vue3 中元素的挂载函数是 mountElement,我们就可以来看看元素指令调 createdbeforeMountmounted 生命周期函数的用相关的调用执行的过程。

3.1 mountElement

function mountElement(vnode: any, container: any, parentComponent, anchor) {
// 创建 DOM 元素节点
const el = (vnode.el = hostCreateElement(vnode.type))
const { props, children, shapeFlag, dirs } = vnode
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 处理子节点是纯文本的情况
el.textContent = children
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 处理子节点是数组的情况
mountChildren(vnode.children, el, parentComponent, anchor)
}
if (dirs) {
/** 执行指令的 created 生命周期的函数 **/
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// 处理 props,比如 class、style、event 等属性
if (props) {
for (const key in props) {
const val = props[key]
hostPatchProp(el, key, null, val)
}
}
if (dirs) {
/** 执行指令的 beforeMount 生命周期的函数 **/
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}
// container.append(el)
// 把创建的 DOM 元素挂载到对应的根节点 container 上
hostInsert(el, container, anchor)

if (dirs) {
queuePostFlushCb(() => {
/** 执行指令的 mounted 生命周期的函数 **/
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
})
}
}

元素 vnode 在挂载的时候,先会创建一个真实 DOM 节点引用 el,然后会去判断子节点的情况,如果是文本则去赋值,如果是数组则循环数组节点进行处理。接着就是处理元素的 props,在处理元素的 props 之前,会执行指令的 created 生命周期的函数,在处理完元素的 props 且在元素插入到容器之前,会执行指令的 beforeMount 生命周期的函数,在元素插入容器之后,会通过一个异步函数来执行指令的 mounted 生命周期的函数。总结如下:

  1. created: 在绑定元素的 attribute 或事件侦听器被应用之前调用。当指令需要添加一些事件侦听器,且这些事件侦听器需要在普通的 v-on 事件侦听器前调用时,可以利用此钩子函数。

  2. beforeMount: 当指令第一次绑定到元素,在挂载到父节点之前调用。

  3. mounted: 在绑定元素的被挂载到父节点后调用且是异步调用执行

3.2 invokeDirectiveHook

指令的生命周期函数的执行是通过调用 invokeDirectiveHook 函数完成的

export function invokeDirectiveHook(
vnode, // 当前 vnode
prevVNode, // 旧 vnode
instance, // 组件实例
name // 指令钩子函数的名称
) {
const bindings = vnode.dirs!
// 获取旧的指令对象
const oldBindings = prevVNode && prevVNode.dirs!
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i]
if (oldBindings) {
// 把旧的指令对象上 value 值赋值给 oldValue
binding.oldValue = oldBindings[i].value
}
let hook = binding.dir[name]
if (hook) {
// 在执行指令生命周期钩子函数之前,先会组装钩子函数的参数,然后传递过去
const args = [
vnode.el,
binding,
vnode,
prevVNode
]
hook(...args)
}
}
}

invokeDirectiveHook 函数主要是根据指令钩子的函数名称把当前节点的 vnode 上的指令取出来执行一遍。具体就是通过遍历 vnode.dirs 数组,找到每一个指令对应 binding 对象,然后从 binding 对象中根据 name 找到指令定义的钩子函数,如果定义了这个钩子函数则执行它。

在执行指令生命周期钩子函数之前,先会组装钩子函数的参数,然后传递过去,这样在每个指令生命周期的钩子函数里面都可以获取到对应的参数了。按照顺序分别对应第一个参数是当前节点的真实 DOM 的引用,第二个参数则是 withDirectives 函数中封装的包含指令对象相关的参数,比如指令的参数,指令的修饰符等等,第三个则是当前节点的虚拟 DOM,第四个是旧虚拟 DOM

四、Update


指令的 beforeUpdateupdated 生命周期函数顾名思义就可以知道它们是在元素更新的时候执行的,具体是因为 v-model 所绑定的状态数据是响应式的,所以状态数据发生了变化就会引起组件的重新渲染。而在 Vue3 中一个元素的更新是通过 patchElement 实现的,所以我们就分析 patchElement 函数中是如何执行指令的钩子函数的。

4.1 patchElement

function patchElement(n1, n2, parentComponent, anchor) {
const { dirs } = n2
/** 执行指令 beforeUpdate 生命周期函数 **/
if (dirs) {
invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
}
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 需要把 el 挂载到新的 vnode
const el = (n2.el = n1.el)
// 对比 children,也就是 diff 发生的地方
patchChildren(n1, n2, el, parentComponent, anchor)
// 对比 props
patchProps(el, oldProps, newProps)

if (dirs) {
queuePostFlushCb(() => {
/** 执行指令 updated 生命周期函数 **/
dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
})
}
}

通过上述代码我们可以清楚看到,在更新子节点之前,会执行指令的 beforeUpdate 钩子函数,在更新完子节点之后,会通过异步函数执行指令的 updated 函数。总结如下:

  • beforeUpdate: 在更新包含此指令元素的 vnode 之前调用。

  • updated: 在包含此指令元素的 vnode 及其子元素的 vnode 更新后调用。

五、UnMount


指令的 beforeUnmountunmounted 生命周期函数顾名思义就可以知道它们是在元素卸载的时候执行的,而 Vue3 元素的卸载是在 unmount 方法中完成的,我们接下来看看具体的实现过程。

5.1 unmount

const unmount = (
vnode,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false
) => {
const {
type ,props, ref, children, dynamicChildren, shapeFlag, patchFlag, dirs
} = vnode
// 是否是元素,是否有指令
const shouldInvokeDirs = shapeFlag & ShapeFlags.ELEMENT && dirs

if (shapeFlag & ShapeFlags.COMPONENT) {
unmountComponent(vnode.component!, parentSuspense, doRemove)
} else {
/** 执行指令 beforeUnmount 生命周期函数 **/
if (shouldInvokeDirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeUnmount')
}

// 一系列删除节点操作
}

if (shouldInvokeDirs) {
queuePostFlushCb(() => {
/** 执行指令 unmounted 生命周期函数 **/
shouldInvokeDirs &&
invokeDirectiveHook(vnode, null, parentComponent, 'unmounted')
})
}
}

unmount 函数的主要作用就是通过递归的方式去遍历删除自身的节点和子节点。可以看到,在移除元素的子节点之前会执行指令的 beforeUnmount 生命周期函数,在移除子节点和当前节点之后,会通过异步的方式执行指令的 unmounted 生命周期函数。总结如下:

  • beforeUnmount: 在卸载绑定元素的父组件之前调用。

  • unmounted: 在指令与元素解除绑定且父组件已卸载时调用。