跳到主要内容

认识

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

一、认识


双向数据绑定 基于 MVVM 模型, 包括数据层Model视图层View业务逻辑层ViewModel。其中 ViewModel(业务逻辑层) 负责将数据和视图关联起来, 提供了数据变化后更新视图视图变化后更新数据这样一个功能,就是传统意义上的双向绑定。

v-model 指令既可以作用于普通表单元素,又可以作用于组件。它是一个语法糖,用于自动实现双向绑定,即数据驱动DOMDOM 的变化反过来影响数据。v-model 实现原理为: 首先 template 是需要被编译成 render 函数,然后在执行 Vue 组件的时候最重要的步骤就是通过执行 render 函数取得 vnode,再通过 vnode 渲染成真实 DOM。如果我们在特定的元素上使用了 v-model 指令之后,在 render 函数中会通过 withDirectives 函数将相关指令对象设置到 vnodedirs 属性上,在元素 vnode 挂载的过程中又会从 vnodedirs 属性中取出来相关的指令对象进行执行。

二、Parse


三、Transform


四、Generate


Vue.js 3.0 会将 v-model = a 根据不同的表单类型, 生成的 render 函数如下所示。v-model 指令则在编译后的 render 函数中需要使用 withDirectives 函数进行处理相关指令,也就是往虚拟 DOM 中添加指令。具体是 inputtextarea 表单元素使用的指令都是 vModelText,单项选择框 radio 使用的指令是 vModelRadio,复选框 checkbox 使用的指令是 vModelCheckbox,下拉选择框 select 使用的指令是 vModelSelect

4.1 input

普通文本输入框 input

<input v-model="state">

编译后的 render 函数

import { vModelText as _vModelText, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return _withDirectives((_openBlock(), _createElementBlock("input", {
"onUpdate:modelValue": $event => ((_ctx.state) = $event)
}, null, 8 /* PROPS */, ["onUpdate:modelValue"])), [
[_vModelText, _ctx.state]
])
}

4.2 textarea

textarea 文本域输入框

<textarea v-model="state"></textarea>

编译后的 render 函数

import { vModelText as _vModelText, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return _withDirectives((_openBlock(), _createElementBlock("textarea", {
"onUpdate:modelValue": $event => ((_ctx.state) = $event)
}, null, 8 /* PROPS */, ["onUpdate:modelValue"])), [
[_vModelText, _ctx.state]
])
}

4.3 radio

单项选择框 radio

<input type="radio" v-model="state">

编译后的 render 函数

import { vModelRadio as _vModelRadio, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return _withDirectives((_openBlock(), _createElementBlock("input", {
type: "radio",
"onUpdate:modelValue": $event => ((_ctx.state) = $event)
}, null, 8 /* PROPS */, ["onUpdate:modelValue"])), [
[_vModelRadio, _ctx.state]
])
}

4.4 checkbox

复选框 checkbox

<input type="checkbox" v-model="state">

编译后的 render 函数

import { vModelCheckbox as _vModelCheckbox, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return _withDirectives((_openBlock(), _createElementBlock("input", {
type: "checkbox",
"onUpdate:modelValue": $event => ((_ctx.state) = $event)
}, null, 8 /* PROPS */, ["onUpdate:modelValue"])), [
[_vModelCheckbox, _ctx.state]
])
}

4.5 select

下拉选择框 select

<select v-model="state"></select>

编译后的 render 函数

import { vModelSelect as _vModelSelect, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return _withDirectives((_openBlock(), _createElementBlock("select", {
"onUpdate:modelValue": $event => ((_ctx.state) = $event)
}, null, 8 /* PROPS */, ["onUpdate:modelValue"])), [
[_vModelSelect, _ctx.state]
])
}

4.6 Component

<UserName
v-model="state"
/>

编译后的 render 函数

import { resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_my_input = _resolveComponent("my-input")

return (_openBlock(), _createBlock(_component_my_input, {
modelValue:: _ctx.state,
"onUpdate:modelValue": $event => ((_ctx.state) = $event)
}, null, 8 /* PROPS */, ["modelValue", "onUpdate:modelValue"]))
}

v-model 指令应用在组件上的时候,就等于是给组件传入了一个名为 modelValueprop,它的值是组件传入的状态变量,此外还会在组件上传入一个名为 onUpdate:modelValue 的监听事件,事件的回调函数拥有一个参数 $event,执行的时候会把参数 $event 赋值给状态变量。

因此, 可以在组件上可以进行多个 v-model 绑定,而在元素上则不可以。

五、Render


如果特定标签元素使用了 v-model 指令则在编译后的 render 函数中需要使用 withDirectives 函数进行处理相关指令,也就是往虚拟 DOM 中添加指令。v-model 指令则在编译后的 render 函数中需要使用 withDirectives 函数进行处理相关指令,也就是往虚拟 DOM 中添加指令。具体是 inputtextarea 表单元素使用的指令都是 vModelText,单项选择框 radio 使用的指令是 vModelRadio,复选框 checkbox 使用的指令是 vModelCheckbox,下拉选择框 select 使用的指令是 vModelSelect

5.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
}

5.1 vModelText

export const vModelText = {
created(el, { modifiers: { lazy, trim, number } }, vnode) {
// 获取当前节点 props 中的 onUpdate:modelValue 更新函数
el._assign = getModelAssigner(vnode)
// 判断是否数字
const castToNumber = number || el.type === 'number'
// 监听当前节点,如果存在 lazy 修饰符则监听 change 事件否则就监听 input 事件。
addEventListener(el, lazy ? 'change' : 'input', e => {
// 如果存在 e.target.composing 存在则返回
if ((e.target as any).composing) return
let domValue = el.value
if (trim) {
// 如果存在 trim 修饰符则执行 trim() 方法去除字符串的头尾空格
domValue = domValue.trim()
} else if (castToNumber) {
// 如果存在 number 修饰符或者是 number 类型的 input 表单则把值转换成数字
domValue = toNumber(domValue)
}
// 更新状态值,也就是用户操作 DOM 后是通过此来反向影响状态值的变化
el._assign(domValue)
})
if (trim) {
// 如果存在 trim 修饰符则监听 change 事件并且把值通过 trim 方法去除字符串的头尾空格
addEventListener(el, 'change', () => {
el.value = el.value.trim()
})
}
if (!lazy) {
// 利用 compositionstart 和 compositionend 控制中文输入的开始和结束动作
addEventListener(el, 'compositionstart', onCompositionStart)
addEventListener(el, 'compositionend', onCompositionEnd)
addEventListener(el, 'change', onCompositionEnd)
}
},
mounted(el, { value }) {
// 更新当前节点真实 DOM 的值
el.value = value == null ? '' : value
},
beforeUpdate(el, { value, modifiers: { trim, number } }, vnode) {
// 获取当前节点 props 中的 onUpdate:modelValue 更新函数
el._assign = getModelAssigner(vnode)
// 如果处于中文输入法的控制状态则不进行更新
if ((el as any).composing) return
// 通过 document.activeElement 可以获取哪个元素获取到了焦点
// focus() 方法可以使某个元素获取焦点
// 如果当前节点是正在被操作也就是获得了焦点就进行相关操作,主要是如果新旧值如果一样则不进行更新操作以节省性能开销
if (document.activeElement === el) {
if (trim && el.value.trim() === value) {
return
}
if ((number || el.type === 'number') && toNumber(el.value) === value) {
return
}
}
const newValue = value == null ? '' : value
if (el.value !== newValue) {
// 将状态值更新到真实 DOM 中
el.value = newValue
}
}
}

created 钩子函数主要逻辑:

  1. 首先执行这 el._assign = getModelAssigner(vnode) 一行代码,这一行代码主要是获取当前节点 props 中的 v-model 绑定的状态更新函数 "onUpdate:modelValue": $event => ((_ctx.state) = $event)

  2. 然后监听当前节点,如果存在 lazy 修饰符则监听 change 事件否则就监听 input 事件。input 事件是实时触发的,就是当输入框里的值发生改变就会立即触发,而 change 事件则需要等到失去焦点才触发。

  3. 在监听的回调函数中最终目的就是为了去更新 v-model 绑定的状态数据,简单来说就是通过当前真实节点的引用也就是 elvalue 值来获取最新的 DOM 值,然后再通过上面已经赋值给 el._assignprops 中的 onUpdate:modelValue 状态更新函数来进行更新应用状态。这就是用户操作 DOM 后状态数据的变化的流程,也就是从 DOM 到数据的变化过程原理。

  4. 此外在更新之前会对当前节点的真实 DOM 的值也就是 el.value,进行判断处理作对应的处理。比如,如果存在 trim 修饰符则执行 trim() 方法去除字符串的头尾空格;如果存在 number 修饰符或者是 number 类型的 input 表单则把值转换成数字。

  5. 当不存在 lazy 修饰符的时候,也就是需要实时监听输入的时候,需要利用 compositionstartcompositionend 监听控制中文输入的开始和结束动作。因为默认输入框并不知道中文输入法的开始和结束。具体的做法就是在 compositionstart 事件的回调函数中对当前节点的真实 DOM 引用的 target 属性上设置一个 composing 开关,当 composingtrue 时,在 input 的事件回调函数中就不去把真实 DOM 的值更新到状态数据上。在 compositionend 事件的回调函数中设置 composingfalse,并且通过手动触发 input 自定义事件,这样再次触发 input 事件的回调函数时,因为 composingfalse,所以就会去把真实 DOM 的值更新到状态数据上。

mounted 钩子函数主要逻辑: 在绑定元素的被挂载到父节点后通过异步调用执行的,主要是因为组件本身的生命周期函数 onMounted 也是异步执行的,所以元素的指令 mounted 函数也需要异步进行执行,这样才能确保所有的节点都已经被挂载完毕,指令做所用的状态数据是最新的。所做的事情很简单,就是把 v-model 绑定的状态数据赋值给绑定的表单元素。我们从前面对 withDirectives 函数的分析中可以知道指令生命周期函数的第二个参数中的 value 属性就是 v-model 所绑定的状态数据。这个也就是初始化的时候 v-model 绑定的数据是怎么被赋值到所绑定的表单元素上的原理。

beforeUpdate 钩子函数主要逻辑: 如果我们在程序里面更改了 v-model 所绑定的状态数据,那么最新的状态数据就是通过指令的 beforeUpdate 生命周期函数更新到所绑定的元素上的。具体是因为 v-model 所绑定的状态数据是响应式的,所以其发生了变化就会引起组件的重新渲染。通过上文我们知道元素的更新是通过 patchElement 函数执行的,在 patchElement 函数内部就会去执行指令的 beforeUpdate 生命周期函数。做的事情其实跟 mounted 是一样的,就是把数据更新到真实 DOM 上。而 beforeUpdate 中在把数据更新到真实 DOM 上之前会做一系列的性能优化操作,主要是如果状态数据和真实 DOM 的值相同则不进行更新操作。

5.2 vModelRadio

export const vModelRadio = {
created(el, { value }, vnode) {
// 给真实 DOM 的 checked 属性赋值
el.checked = looseEqual(value, vnode.props!.value)
// 获取当前节点 props 中的 onUpdate:modelValue 更新函数
el._assign = getModelAssigner(vnode)
// 单项选择只需要监听 change 事件
addEventListener(el, 'change', () => {
// 更新状态值,也就是用户操作 DOM 后是通过此来反向影响状态值的变化
el._assign(getValue(el))
})
},
beforeUpdate(el, { value, oldValue }, vnode) {
// 获取当前节点 props 中的 onUpdate:modelValue 更新函数
el._assign = getModelAssigner(vnode)
// 新老值是否相等
if (value !== oldValue) {
// 将状态值更新到真实 DOM 中
el.checked = looseEqual(value, vnode.props!.value)
}
}
}

created 钩子函数主要逻辑: 通过 looseEqual 函数进行处理当前节点真实 DOMchecked 属性值。其中第一个参数 value,从前文我们可以知道是 v-model 绑定的值,也就是上面的 state,而 vnode.props.value 则是表单设置的 value 值。也就是判断这两个值是否相等,如果相等就返回 true 赋值给真实 DOMchecked 属性上,从而单项选择框处于选中状态。单项选择只需要监听 change 事件,然后在回调函数中通过获取到的当前节点 props 中的 onUpdate:modelValue 更新函数进行更新状态值,也就是用户点击选项选择表单之后,从真实 DOM 的变化到数据变化的流程。

beforeUpdate 钩子函数主要逻辑: 如果我们在程序里面更改了 v-model 所绑定的状态数据,那么最新的状态数据就是通过指令的 beforeUpdate 生命周期函数更新到所绑定的元素上的。

5.3 vModelCheckbox

使用 true-valuefalse-value 自定义 checkbox 的布尔绑定值: true-valuefalse-valueVue 特有的 attributes,仅支持和 v-model 配套使用。这里 toggle 属性的值会在选中时被设为 yes,取消选择时设为 no

<input
type="checkbox"
v-model="toggle"
true-value="yes"
false-value="no" />
export const vModelCheckbox = {
created(el, _, vnode) {
// 获取当前节点 props 中的 onUpdate:modelValue 更新函数
el._assign = getModelAssigner(vnode)
addEventListener(el, 'change', () => {
// _modelValue 就是 v-model 绑定的状态数据
const modelValue = (el as any)._modelValue
// 获取 DOM 实例上 value 值
const elementValue = getValue(el)
// 选中状态
const checked = el.checked
const assign = el._assign
// 处理 modelValue 是数组的情况
if (isArray(modelValue)) {
// 获取当前选项在 modelValue 数组中的位置
const index = looseIndexOf(modelValue, elementValue)
const found = index !== -1
if (checked && !found) {
// 如果是选中状态且 modelValue 里不存在当前 DOM 实例上 value 值,就往 modelValue 上添加,并且更新状态数据
assign(modelValue.concat(elementValue))
} else if (!checked && found) {
// 如果是不是选中状态,又在 modelValue 中找到当前选项的值,则需要把当前选项的值从 modelValue 中删除,并且更新状态数据
const filtered = [...modelValue]
filtered.splice(index, 1)
assign(filtered)
}
} else if (isSet(modelValue)) {
// 如果是 Set 的数据类型的处理方案
const cloned = new Set(modelValue)
if (checked) {
// 如果是选中状态则添加
cloned.add(elementValue)
} else {
// 如果是未选中状态则删除
cloned.delete(elementValue)
}
assign(cloned)
} else {
// 不是多个复选项的情况,处理的过程就跟单项选择框 Radio 一样。
assign(getCheckboxValue(el, checked))
}
})
},
// 这里需要在 mounted 生命周期里初始化是因为需要等 true-value/false-value 的 props 设置完毕
mounted: setChecked,
beforeUpdate(el, binding, vnode) {
// 获取当前节点 props 中的 onUpdate:modelValue 更新函数
el._assign = getModelAssigner(vnode)
// 更新过程跟初始化过程一样
setChecked(el, binding, vnode)
}
}

created 钩子函数主要逻辑: 监听 change 事件,在 change 事件的回调函数中,获取 v-model 绑定的状态数据也就是 modelValue,获取复选框 DOM 实例上 value 值,以及 复选框 DOM 实例的 checked 属性值。然后根据 modelValue 的数据类型进行不同的处理。

mounted 钩子函数主要逻辑: 复选框的数据初始化是需要通过 mounted 函数来实现的,因为需要等待 true-valuefalse-valueprops 设置完毕。created 函数是在创建元素 DOM 实例之后执行的,此时还没设置元素 DOM 实例中的 attributes 数据,所以在 created 函数是获取不到 true-valuefalse-value 的值的。mounted 函数是已经完全初始化了元素的 DOM 实例之后,并且通过异步执行确保所有的应用节点都挂载完成后执行的,所以在 mounted 函数中是可以获取到 true-valuefalse-value 的值的。同时需要在 mounted 函数中把 v-model 绑定的状态数据设置到元素实例对象 el_modelValue 属性上,以供在 created 函数中监听的 change 事件的回调函数中使用。

beforeUpdate 钩子函数主要逻辑: beforeUpdate 函数的处理过程跟 mounted 函数一致

5.4 vModelSelect

export const vModelSelect = {
created(el, { value, modifiers: { number } }, vnode) {
// 判断 v-model 绑定的状态数据是否 Set 类型
const isSetModel = isSet(value)
addEventListener(el, 'change', () => {
// 通过 Array.prototype.filter.call 方法筛选选中的选项数据,返回值是数组
const selectedVal = Array.prototype.filter
.call(el.options, (o: HTMLOptionElement) => o.selected)
.map(
(o: HTMLOptionElement) =>
// 如果存在 number 修饰器则对返回值进行数字化处理
number ? toNumber(getValue(o)) : getValue(o)
)
// 更新 v-model 绑定的状态数据
el._assign(
el.multiple
? isSetModel
? new Set(selectedVal) // 如果多选且是 Set 类型则返回 Set 类型数据
: selectedVal // 如果是多选其是数组
: selectedVal[0] // 因为上面经过处理返回的数据是数组
)
})
// 获取当前节点 props 中的 onUpdate:modelValue 更新函数
el._assign = getModelAssigner(vnode)
},
// 设置 value 值需要在 mounted 方法和 updated 方法中,因为需要等待子元素 option 也渲染完毕
mounted(el, { value }) {
setSelected(el, value)
},
beforeUpdate(el, _binding, vnode) {
// 更新当前节点 props 中的 onUpdate:modelValue 更新函数
el._assign = getModelAssigner(vnode)
},
updated(el, { value }) {
setSelected(el, value)
}
}

created 钩子函数主要逻辑: 首先设置 isSetModel 判断 v-model 绑定的状态数据是否 Set 类型,在 change 事件的回调函数中通过 Array.prototype.filter.call 方法筛选选中的选项数据,因为返回值是数组,所以继续使用 map 通过链式调用处理返回的数组内容,主要是使用 getValue 函数获取 option 选项中 value 值,因为跟单选框一样 selectoption 标签中设置的 value 值最终会被 Vue3 处理成 _value 属性挂载 option 元素实例对象 el 上。再判断是否存在 number 修饰符,如果存在则还需要把 option 上的值转成 number 类型。最后通过 props 中的 onUpdate:modelValue 更新函数更新状态值,对更新的值还需要根据不同的情况进行处理。如果是多选且更新的值是 Set 类型则返回 Set 类型数据,如果是多选且更新的值是数组则不用做额外处理,如果是单项,则把数组的第0项返回即可,因为上面经过处理返回的数据是数组。

mounted 钩子函数主要逻辑: 在 mounted 函数中主要做的工作就是初始化,也就是设置 selectoption 标签哪个处于被选中状态。由于 option 标签是 select 标签的子元素,所以需要等 option 标签也渲染完毕才能进行设置,所以就需要在 mounted 方法里设置了,同样更新也一样,跟文本框指令、单选框指令、复选框指令的更新是在 beforeUpdate 函数中处理不同,选择器指令的更新需要在 updated 函数中处理。因为 mountedupdated 都是通过异步调用执行的,所以根据 Vue3 运行流程 mountedupdated 方法执行的时候当前所有的节点挂载或更新完毕了。

beforeUpdate 钩子函数主要逻辑: 去更新当前节点 props 中的 onUpdate:modelValue 更新函数

update 钩子函数主要逻辑: updated 函数所做的事情跟 mounted 一样

六、Mount

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

6.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: 在绑定元素的被挂载到父节点后调用且是异步调用执行

6.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 函数中是如何执行指令的钩子函数的。

7.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 方法中完成的,我们接下来看看具体的实现过程。

8.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: 在指令与元素解除绑定且父组件已卸载时调用。