认识
一、认识
双向数据绑定 基于 MVVM
模型, 包括数据层Model
、视图层View
、业务逻辑层ViewModel
。其中 ViewModel
(业务逻辑层) 负责将数据和视图关联起来, 提供了数据变化后更新视图和视图变化后更新数据这样一个功能,就是传统意义上的双向绑定。
v-model
指令既可以作用于普通表单元素,又可以作用于组件。它是一个语法糖,用于自动实现双向绑定,即数据驱动DOM
,DOM
的变化反过来影响数据。v-model
实现原理为: 首先 template
是需要被编译成 render
函数,然后在执行 Vue
组件的时候最重要的步骤就是通过执行 render
函数取得 vnode
,再通过 vnode
渲染成真实 DOM
。如果我们在特定的元素上使用了 v-model
指令之后,在 render
函数中会通过 withDirectives
函数将相关指令对象设置到 vnode
的 dirs
属性上,在元素 vnode
挂载的过程中又会从 vnode
的 dirs
属性中取出来相关的指令对象进行执行。
二、Parse
三、Transform
四、Generate
Vue.js 3.0
会将 v-model = a
根据不同的表单类型, 生成的 render
函数如下所示。v-model
指令则在编译后的 render
函数中需要使用 withDirectives
函数进行处理相关指令,也就是往虚拟 DOM
中添加指令。具体是 input
和 textarea
表单元素使用的指令都是 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
指令应用在组件上的时候,就等于是给组件传入了一个名为 modelValue
的 prop
,它的值是组件传入的状态变量,此外还会在组件上传入一个名为 onUpdate:modelValue
的监听事件,事件的回调函数拥有一个参数 $event
,执行的时候会把参数 $event
赋值给状态变量。
因此, 可以在组件上可以进行多个 v-model
绑定,而在元素上则不可以。
五、Render
如果特定标签元素使用了 v-model
指令则在编译后的 render
函数中需要使用 withDirectives
函数进行处理相关指令,也就是往虚拟 DOM
中添加指令。v-model
指令则在编译后的 render
函数中需要使用 withDirectives
函数进行处理相关指令,也就是往虚拟 DOM
中添加指令。具体是 input
和 textarea
表单元素使用的指令都是 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
钩子函数主要逻辑:
-
首先执行这
el._assign = getModelAssigner(vnode)
一行代码,这一行代码主要是获取当前节点props
中的v-model
绑定的状态更新函数"onUpdate:modelValue": $event => ((_ctx.state) = $event)
。 -
然后监听当前节点,如果存在
lazy
修饰符则监听change
事件否则就监听input
事件。input
事件是实时触发的,就是当输入框里的值发生改变就会立即触发,而change
事件则需要等到失去焦点才触发。 -
在监听的回调函数中最终目的就是为了去更新
v-model
绑定的状态数据,简单来说就是通过当前真实节点的引用也就是el
的value
值来获取最新的DOM
值,然后再通过上面已经赋值给el._assign
的props
中的onUpdate:modelValue
状态更新函数来进行更新应用状态。这就是用户操作DOM
后状态数据的变化的流程,也就是从DOM
到数据的变化过程原理。 -
此外在更新之前会对当前节点的真实
DOM
的值也就是el.value
,进行判断处理作对应的处理。比如,如果存在trim
修饰符则执行trim()
方法去除字符串的头尾空格;如果存在number
修饰符或者是number
类型的input
表单则把值转换成数字。 -
当不存在
lazy
修饰符的时候,也就是需要实时监听输入的时候,需要利用compositionstart
和compositionend
监听控制中文输入的开始和结束动作。因为默认输入框并不知道中文输入法的开始和结束。具体的做法就是在compositionstart
事件的回调函数中对当前节点的真实DOM
引用的target
属性上设置一个composing
开关,当composing
为true
时,在input
的事件回调函数中就不去把真实 DOM 的值更新到状态数据上。在compositionend
事件的回调函数中设置composing
为false
,并且通过手动触发input
自定义事件,这样再次触发input
事件的回调函数时,因为composing
为false
,所以就会去把真实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
函数进行处理当前节点真实 DOM
的 checked
属性值。其中第一个参数 value
,从前文我们可以知道是 v-model
绑定的值,也就是上面的 state
,而 vnode.props.value
则是表单设置的 value
值。也就是判断这两个值是否相等,如果相等就返回 true
赋值给真实 DOM
的 checked
属性上,从而单项选择框处于选中状态。单项选择只需要监听 change
事件,然后在回调函数中通过获取到的当前节点 props
中的 onUpdate:modelValue
更新函数进行更新状态值,也就是用户点击选项选择表单之后,从真实 DOM
的变化到数据变化的流程。
beforeUpdate
钩子函数主要逻辑: 如果我们在程序里面更改了 v-model
所绑定的状态数据,那么最新的状态数据就是通过指令的 beforeUpdate
生命周期函数更新到所绑定的元素上的。
5.3 vModelCheckbox
使用 true-value
和 false-value
自定义 checkbox
的布尔绑定值: true-value
和 false-value
是 Vue
特有的 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-value
和 false-value
的 props
设置完毕。created
函数是在创建元素 DOM
实例之后执行的,此时还没设置元素 DOM
实例中的 attributes
数据,所以在 created
函数是获取不到 true-value
和 false-value
的值的。mounted
函数是已经完全初始化了元素的 DOM
实例之后,并且通过异步执行确保所有的应用节点都挂载完成后执行的,所以在 mounted
函数中是可以获取到 true-value
和 false-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
值,因为跟单选框一样 select
中 option
标签中设置的 value
值最终会被 Vue3
处理成 _value
属性挂载 option
元素实例对象 el
上。再判断是否存在 number
修饰符,如果存在则还需要把 option
上的值转成 number
类型。最后通过 props
中的 onUpdate:modelValue
更新函数更新状态值,对更新的值还需要根据不同的情况进行处理。如果是多选且更新的值是 Set
类型则返回 Set
类型数据,如果是多选且更新的值是数组则不用做额外处理,如果是单项,则把数组的第0
项返回即可,因为上面经过处理返回的数据是数组。
mounted
钩子函数主要逻辑: 在 mounted
函数中主要做的工作就是初始化,也就是设置 select
的 option
标签哪个处于被选中状态。由于 option
标签是 select
标签的子元素,所以需要等 option
标签也渲染完毕才能进行设置,所以就需要在 mounted
方法里设置了,同样更新也一样,跟文本框指令、单选框指令、复选框指令的更新是在 beforeUpdate
函数中处理不同,选择器指令的更新需要在 updated
函数中处理。因为 mounted
和 updated
都是通过异步调用执行的,所以根据 Vue3
运行流程 mounted
和 updated
方法执行的时候当前所有的节点挂载或更新完毕了。
beforeUpdate
钩子函数主要逻辑: 去更新当前节点 props
中的 onUpdate:modelValue
更新函数
update
钩子函数主要逻辑: updated
函数所做的事情跟 mounted
一样
六、Mount
在 render
函数中已经通过 withDirectives
函数将相关指令对象设置到了 vnode
的 dirs
属性上。所以在元素 vnode
挂载的过程中又会从 vnode
的 dirs
属性中取出来相关的指令对象进行执行。在 Vue3
中元素的挂载函数是 mountElement
,我们就可以来看看元素指令调 created
、beforeMount
、mounted
生命周期函数的用相关的调用执行的过程。
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
生命周期的函数。总结如下:
-
created
: 在绑定元素的attribute
或事件侦听器被应用之前调用。当指令需要添加一些事件侦听器,且这些事件侦听器需要在普通的v-on
事件侦听器前调用时,可以利用此钩子函数。 -
beforeMount
: 当指令第一次绑定到元素,在挂载到父节点之前调用。 -
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
指令的 beforeUpdate
、updated
生命周期函数顾名思义就可以知道它们是在元素更新的时候执行的,具体是因为 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
指令的 beforeUnmount
、unmounted
生命周期函数顾名思义就可以知道它们是在元素卸载的时候执行的,而 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
: 在指令与元素解除绑定且父组件已卸载时调用。