v-model 指令的实现详解我们通过上文知道 v-model 应用在特定的表单元素标签上会绑定不同的指令对象具体是 input 和 textarea 表单元素使用的指令都是 vModelText单项选择框 radio 使用的指令是 vModelRadio复选框 checkbox 使用的指令是 vModelCheckbox下拉选择框 select 使用的指令是 vModelSelect。vModelText 指令接下来我们进行详细分析 vModelText 指令。vModelTex 指令源码如下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 生命周期函数是在元素 vnode 创建真实 DOM 引用之后通过 invokeDirectiveHook 函数调用执行的。在调用指令的 created 生命周期函数之前会构造指令生命周期函数所需的参数并传递过来。按照顺序分别对应第一个参数是当前节点的真实 DOM 的引用第二个参数则是 withDirectives 函数中封装的包含指令对象相关的参数比如指令的参数指令的修饰符等等第三个则是当前节点的虚拟 DOM第四个是旧虚拟 DOM。这样我们就可以在指令的生命周期函数里面获取到当前节点的相关信息了。created 函数在指令的 created 函数中我们首先执行这el._assign getModelAssigner(vnode)一行代码这一行代码主要是获取当前节点 props 中的v-model绑定的状态更新函数我们回顾上文每一个应用了v-model的元素标签编译成 render 函数之后都会有一个onUpdate:modelValue: $event ((_ctx.state) $event)的 props这个就是当前节点v-model绑定的状态更新函数。const getModelAssigner (vnode) { const fn vnode.props![onUpdate:modelValue] return isArray(fn) ? value invokeArrayFns(fn, value) : fn }有可能存在多个状态更新函数如果是多个状态更新函数则则封装一个高级函数进行调用处理。在 Vue 官网中对v-model的 lazy 修饰符有以下的介绍在默认情况下v-model在input事件中同步输入框的值与数据 (除了 上述 IME 部分)但你可以添加一个修饰符lazy从而转变为在change事件中同步。我们通过观察源码可以更深刻理解 lazy 修饰符的作用。当存在 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 函数我们通过上文可以知道 mounted 函数是在绑定元素的被挂载到父节点后通过异步调用执行的主要是因为组件本身的生命周期函数 onMounted 也是异步执行的所以元素的指令 mounted 函数也需要异步进行执行这样才能确保所有的节点都已经被挂载完毕指令做所用的状态数据是最新的。vModelText 指令中 mounted 函数所做的事情很简单就是把v-model绑定的状态数据赋值给绑定的表单元素。我们从前面对 withDirectives 函数的分析中可以知道指令生命周期函数的第二个参数中的 value 属性就是v-model所绑定的状态数据。这个也就是初始化的时候v-model绑定的数据是怎么被赋值到所绑定的表单元素上的原理。beforeUpdate 函数如果我们在程序里面更改了v-model所绑定的状态数据那么最新的状态数据就是通过指令的 beforeUpdate 生命周期函数更新到所绑定的元素上的。具体是因为v-model所绑定的状态数据是响应式的所以其发生了变化就会引起组件的重新渲染。通过上文我们知道元素的更新是通过 patchElement 函数执行的在 patchElement 函数内部就会去执行指令的 beforeUpdate 生命周期函数。beforeUpdate 最终做的事情其实跟 mounted 是一样的就是把数据更新到真实 DOM 上。而 beforeUpdate 中在把数据更新到真实 DOM 上之前会做一系列的性能优化操作主要是如果状态数据和真实 DOM 的值相同则不进行更新操作。以上便是 vModelText 指令的实现原理分析总结接下来我们分析 vModelRadio 指令有了 vModelText 指令的基础后面的我们就不进行十分详细的分析了只做简单的分析。vModelRadio 指令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) } } }vModelRadio 指令的实现相对还是比较简单的只需要 created 和 beforeUpdate 两个生命周期就实现了。created 函数在分析 created 函数之前我们先回顾一下单项选择框的使用input typeradio value1 v-modelstate我们先会给单项选择表单设置一个 value 值当 state 的值等于 value 的值的时候单项选择框处于选中状态也就是 checked 属性值为 true。所以在初始化的时候首先是通过 looseEqual 函数进行处理当前节点真实 DOM 的 checked 属性值其中第一个参数 value从前文我们可以知道是v-model绑定的值也就是上面的 state而 vnode.props.value 则是表单设置的 value 值。也就是判断这两个值是否相等如果相等就返回 true 赋值给真实 DOM 的 checked 属性上从而单项选择框处于选中状态。单项选择只需要监听 change 事件然后在回调函数中通过获取到的当前节点 props 中的 onUpdate:modelValue 更新函数进行更新状态值也就是用户点击选项选择表单之后从真实 DOM 的变化到数据变化的流程。这里有一个点需要值得说明的是 getValue 函数的实现。function getValue(el) { return _value in el ? (el as any)._value : el.value }我们可以看到 getValue 函数中是通过判断真实 DOM 引用 el 中是否存在_value属性如果存在就获取_value属性值否则就获取 el 本身的 value 值。这是为什么呢我们来看看下面的代码// packages\runtime-dom\src\modules\props.ts export function patchDOMProp(el, key, value) { if (key value) { el._value value } }从上面代码我们可以看到在 Vue3 中在元素上设置绑定 value 值时会被设置到一个自定义属性_value上此外还有 select 标签中 option 选项中设置的 value 值也是同样的处理。此外当我们在普通文本输入框进行以上操作时input typetext :valueval v-modelstate这个时候编译器会报错内部服务器错误在使用 v-model 的同时存在不必要值绑定。这会干扰 v-model 的行为Internal server error: Unnecessary value binding used alongside v-model. It will interfere with v-models behavior.所以在 vModelText 指令中是不需要对获取真实 DOM 的 value 进行特别处理的。beforeUpdate 函数从前文我们可以知道如果我们在程序里面更改了v-model所绑定的状态数据那么最新的状态数据就是通过指令的 beforeUpdate 生命周期函数更新到所绑定的元素上的。vModelRadio 指令的 beforeUpdate 函数的实现很简单通过前文的学习我们可以很容易看懂没有太多好说的。vModelCheckbox 指令复选框的使用方式使用 true-value 和 false-value 自定义 checkbox 的布尔绑定值input typecheckbox v-modeltoggle true-valueyes false-valueno /true-value和false-value是 Vue 特有的 attributes仅支持和v-model配套使用。这里toggle属性的值会在选中时被设为yes取消选择时设为no。上述例子来自 Vue 官网更多详细请查看官网例子。此外还有将多个复选框绑定到同一个数组或集合的值input typecheckbox idjack valueJack v-modelcheckedNames input typecheckbox idjohn valueJohn v-modelcheckedNames input typecheckbox idmike valueMike v-modelcheckedNames如果选中了复选框表单中设置的值就会添加到状态变量 checkedNames 数组中也可以说状态变量 checkedNames 数组中存在哪些元素值那么哪些复选框则是选中状态。还有单一的复选框绑定布尔类型值input typecheckbox idcheckbox v-modelchecked /如果选中状态变量 checked 为 true未选中则为 false。以上就是复选框在 Vue 中使用方式以及特别的使用方式 “使用 true-value 和 false-value 自定义 checkbox 的布尔绑定值”所以我们需要先了解这些前置知识。vModelCheckbox 指令的特别地方了解完了这些我们再来看看 vModelCheckbox 指令的源码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) } }由于复选框的使用方式不同所以 vModelCheckbox 指令需要真监听的 change 事件的回调函数中使用v-model绑定的状态数据如果是通过 created 生命周期中参数中获取v-model绑定的状态数据则会是一个闭包变量且每次重新渲染更新生产的 vnode 都会重新创建新的v-model绑定的状态数据所以通过 created 生命周期中参数中获取v-model绑定的状态数据将不会是最新的数据所以需要把v-model绑定的状态数据设置到一个全局变量中去。 vModelCheckbox 指令中的处理方案则是在 mounted 钩子函数和 beforeUpdate 钩子函数中把v-model绑定的状态数据设置到节点元素 DOM 的实例对象 el 上的 _modelValue 属性上。created 函数复选框只需要监听 change 事件在 change 事件的回调函数中获取v-model绑定的状态数据也就是 modelValue获取复选框 DOM 实例上 value 值以及 复选框 DOM 实例的 checked 属性值。然后根据 modelValue 的数据类型进行不同的处理。modelValue 的数据类型是数组通过获取当前选项在 modelValue 数组中的位置判断当前复选框的选项是否存在 modelValue 数组中。如果不存在且当前复选框是选中状态则需要把当前复选框的选项值添加到 modelValue 数组中然后通过更新函数更新v-model中绑定的状态数据。如果当前选项存在 modelValue 数组中且当前复选框是未选中状态则需要把当前复选框的选项值从 modelValue 数组中删除然后通过更新函数更新v-model中绑定的状态数据。modelValue 的数据类型是 Set由于 Set 类型具体天然的去重功能所以 modelValue 的数据类型是 Set 的情况处理起来比数据类型是数组的情况要简单很多也就是如果复选框是选中状态则把复选框的选项值添加到 modelValue 中如果复选框不是选中状态则把复选框的选项值从 modelValue 中删除然后通过更新函数更新v-model中绑定的状态数据。如果是单一的复选框modelValue 的数据类型不是数组也不是 Set 那么就是单一的复选框情况。如果是单一的复选框就通过 getCheckboxValue 函数进行处理通过上文我们知道在 Vue 中可以使用 true-value 和 false-value 自定义 checkbox 的布尔绑定值。这时就需要在 getCheckboxValue 函数中进行处理了。getCheckboxValue 函数源码function getCheckboxValue(el, checked) { const key checked ? _trueValue : _falseValue // 如果 _trueValue 或者 _falseValue 存在 el 实例中则使用 _trueValue 或 _falseValue 的值 return key in el ? el[key] : checked }首先根据复选框的选中状态是判断是获取_trueValue还是_falseValue作为 key然后判断复选框 DOM 实例 el 上有没有存在这两个属性如果存在就返回这两个属性对应的属性值否则返回当前复选框的选中状态。值得注意的是当我们使用使用 true-value 和 false-value 自定义 checkbox 的布尔绑定值时Vue3 底层会在设置元素实例的 attributes 时会把 true-value 和 false-value 属性设置成_trueValue还是_falseValue挂在元素实例对象上。以下是 Vue3 中具体的代码实现的地方。// packages\runtime-dom\src\patchProp.ts export const patchProp ( el, key, prevValue, nextValue, // ... ) { if (key class) { // ... } else if (key style) { // ... } else { if (key true-value) { ;(el as any)._trueValue nextValue } else if (key false-value) { ;(el as any)._falseValue nextValue } // ... } }以上就是用户点击复选框之后从真实 DOM 的变化到数据变化的流程。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 事件的回调函数中使用。mounted 函数的具体实现是通过封装一个 setChecked 函数来实现的因为 mounted 函数和 beforeUpdate 函数是实现过程是一致的所以通过封装共同的函数达到共享逻辑的目的。以下是 setChecked 函数的源码function setChecked(el, { value, oldValue }, vnode) { // 把 v-model 的状态变量设置到 el._modelValue 上相当于是一个全局变量 ;(el as any)._modelValue value if (isArray(value)) { // 如果是数组则判断 v-model 绑定是状态数据中是否存在当前复选框中的选项值也就是 value 值 el.checked looseIndexOf(value, vnode.props!.value) -1 } else if (isSet(value)) { // 如果是 Set 数据则判断 v-model 绑定是状态数据中是否存在当前复选框中的设置的 value 值 el.checked value.has(vnode.props!.value) } else if (value ! oldValue) { // 如果是单一的复选框的情况还需要处理使用 true-value 和 false-value 自定义 checkbox 的布尔绑定值的情况 el.checked looseEqual(value, getCheckboxValue(el, true)) } }具体是根据v-model中绑定的状态数据 value 的类型来分别做不同的处理。value 是数组则通过 looseIndexOf 函数判断 value 中是否存在当前复选框中的选项值也就是 value 值存在为 true不存在为 false。value 是 Set 则通过 Set 的 has 方法判断 value 中是否存在当前复选框中的选项值也就是 value 值存在为 true不存在为 false如果 value 既不是数组又不是 Set 类型那么就去判断新老 value 值是否相同如果不相同则通过 looseEqual 函数判断 value 值与真实 DOM 实例对象的选项值是否相同。旧 value 值是在调用指令钩子函数的方法 invokeDirectiveHook 中实现赋值的。export function invokeDirectiveHook( vnode, prevVNode, 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) { // 把老虚拟 DOM 的指令对象上 value 赋值给新指令对象上 oldValue 属性。 binding.oldValue oldBindings[i].value } // ... } }beforeUpdate 函数beforeUpdate 函数的处理过程跟 mounted 函数一致这里就不再进行赘述了。至此 vModelCheckbox 指令就分析完毕了。