Vue的双向绑定是指数据变化能引起界面的变化,界面数据的变化也能驱动数据的改变。
这个功能其实和单向数据流规范不一样,所以开始接触Vue的时候非常吸引我的一个功能。我们发现Element UI 的表单也有大量使用v-model 进行双向绑定。
双向绑定 其实 不是所有的元素/组件都支持的,目前Vue 支持 input ,select , checkbox , radio 和组件 利用 v-model 指令进行 双向绑定。
我以前对 双向绑定 这个功能有很大的一个疑惑:就是双向绑定为什么不会造成更新死循环?即 界面变化 -> 数据变化 -> 界面变化 -> 数据变化 -> …
v-model 对表单元素进行双向绑定
由于不同的表单元素使用的内部指令是不一样的,我们就用input 作为例子进行分析,其他的表单元素的双向绑定原理非常类似。
这一节涉及到 指令 和 事件处理 相关的知识点,如果不是太清楚的话,建议参阅我前面的两篇相关内容,否则有可能会有一些的疑惑。
案例分析
<input v-model="value" />
<div>{{ value }}</div>
setup() {
let value = ref("");
return {
value
};
}
简单几行代码就实现了input 表单元素和数据value 的双向绑定功能。
代码分析
我们来看看渲染函数
const _hoisted_1 = ["onUpdate:modelValue"]
_withDirectives(_createElementVNode("input", {
"onUpdate:modelValue": $event => (value = $event)
}, null, 8 /* PROPS */, _hoisted_1), [
[_vModelText, value]
])
我们分析withDirectives 函数,看到input 生成的VNode 使用了vModelText 这个内部指令,且添加了一个名为onUpdate:modelValue 的事件处理的pro 函数,onUpdate:modelValue 函数用来修改value 值;
vModelText 指令
export const vModelText: ModelDirective<
HTMLInputElement | HTMLTextAreaElement
> = {
created(el, { modifiers: { lazy, trim, number } }, vnode) {
// 获取到 vnode.props!['onUpdate:modelValue'] 对应的函数
el._assign = getModelAssigner(vnode)
const castToNumber =
number || (vnode.props && vnode.props.type === 'number')
// 如果 有lazy修饰符 监听 input 的 change 事件,否则监听 input 的 input 事件
addEventListener(el, lazy ? 'change' : 'input', e => {
let domValue: string | number = el.value
if (trim) {
// 如果有trim修饰符,则将 input的value进行去空格
domValue = domValue.trim()
} else if (castToNumber) {
// 如果有number修饰符,或者 input 类型是 number类型,则把 input的value变成number类型
domValue = toNumber(domValue)
}
// 然后进行参数的回调实现 界面 到 数据的更改
el._assign(domValue)
})
},
beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
// 更新 'onUpdate:modelValue' 函数,因为有可能不会更新数据,所以
el._assign = getModelAssigner(vnode)
// 如果 input的值没变,不进行任何操作
if (document.activeElement === el) {
if (lazy) {
return
}
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) {
el.value = newValue
}
}
}
created 钩子函数中,如果有lazy 修饰符,input 表单监听change事件,否则监听input事件;beforeUpdate 钩子函数中,要重新获取onUpdate:modelValue 函数,因为重新渲染函数可能更改了这个函数,并且重新给input 赋值;input 中输入新的内容后,如果有trim 修饰符就进行去空格,如果有有number 修饰符或者 input 类型是number 类型需要转换成number,然后通过onUpdate:modelValue 对应的函数修改value 值。
总结:
- 数据->DOM: 响应式数据
value 变化触发组件更新,input的内容将发现变化; - DOM->数据:
vModelText 指令实现了对input的value 变化的监听,根据vModelText 指令的修饰符处理完input的value 值,然后通过onUpdate:modelValue 对应的函数$event => (value = $event) ,重新完成响应式数据value 的修改。响应式数据的修改会触发组件更新。
一些思考
为什么不会出现更新循环呢?
input输入数据 -> 数据处理 -> 调用onUpdate:modelValue 对应的$event => (inputValue = $event) 方法 -> 响应式数据变化触发组件更新 -> input设置新值input.value = newValue 更新至此终止。
为什么更新input 的新值放在vModelText 指令的beforeUpdate 中执行?
指令的更新有两个方法:beforeUpdate 和updated 。 在beforeUpdate 中执行有两个优势:
- 在更新DOM前更新
input 的新值,如果只是修改了input 值,就省去了patchProp 的部分操作,提高了patch 性能; - 指令的
beforeUpdate 是DOM更新前同步执行的,而updated 钩子函数是在DOM更新后异步执行的,如果业务复杂同步任务太多的情况下可能会出现更新延迟或者卡顿的现象。
v-model 对组件进行双向绑定
<Son v-model="modelVlue" />
其实等同于:
<Son
:modelValue="modelVlue"
@update:modelValue="modelVlue = $event"
></Son>
v-model 对组件进行双向绑定 本质上就是一个 语法糖,通过pro 给子组件传递数据,子组件通过v-on 进行事件绑定可以进行数据的修改。
|