认识
一、认识
双向数据绑定 基于 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
生命周期函数更新到所绑定的元素上的。