Vue.js 3.x 双向绑定原理
什么是双向绑定?
废话不多说,我们先来看一个 v-model
基本的示例:
<input type="text" v-model="search">
首先,我们要明白一点的是:v-model
的本质是指令。因此,它跟我们一般的自定义指令是一样的,需要实现 Vue.js
生命周期的钩子函数。
其次,v-model
实现了双向绑定,也就是:数据到 DOM 的单向流动、DOM 到数据的单向流动。
明白了上面这两点,再来看代码就清晰多了。
// packages/runtime-dom/src/directives/vModel.ts export const vModelText: ModelDirective< HTMLInputElement | HTMLTextAreaElement > = { created() {}, mounted() {}, beforeUpdate() {} }
打开 v-model
的源码我们可以看到,它实现了对应的 Vue.js
生命周期钩子函数,实际上它就是一个内置的自定义指令。
那么,v-model
如何实现双向绑定的呢?具体来说,数据到 DOM 的单向流动以及DOM 到数据的单向流动是如何实现的。
数据到 DOM 的单向流动
// packages/runtime-dom/src/directives/vModel.ts export const vModelText: ModelDirective< HTMLInputElement | HTMLTextAreaElement > = { // set value on mounted so it's after min/max for type="range" mounted(el, { value }) { el.value = value == null ? '' : value } }
数据到 DOM 的单向流动实现非常简单,一行代码就搞定了,就是把 v-model
绑定的值赋值给 el.value
。
DOM 到数据的单向流动
// packages/runtime-dom/src/directives/vModel.ts export const vModelText: ModelDirective< HTMLInputElement | HTMLTextAreaElement > = { created(el, { modifiers: { lazy, trim, number } }, vnode) { el._assign = getModelAssigner(vnode) // see: https://github.com/vuejs/core/issues/3813 const castToNumber = number || (vnode.props && vnode.props.type === 'number') // 实现 lazy 功能 addEventListener(el, lazy ? 'change' : 'input', e => { // `composing=true` 时不把 DOM 的值赋值给数据 if ((e.target as any).composing) return let domValue: string | number = el.value if (trim) { domValue = domValue.trim() } else if (castToNumber) { domValue = toNumber(domValue) } // DOM 的值改变时,同时改变对应的数据(即改变 v-model 上绑定的变量的值) el._assign(domValue) }) // 实现 trim 功能 if (trim) { addEventListener(el, 'change', () => { el.value = el.value.trim() }) } // 不配置 lazy 时,监听的是 input 的 input 事件,它会在用户实时输入的时候触发。 // 此外,还会多监听 compositionstart 和 compositionend 事件。 if (!lazy) { // 这是因为,用户使用拼音输入法开始输入汉字时,这个事件会被触发, // 此时,设置 `composing=true`,在 input 事件回调里可以进行判断,避免将 DOM 的值赋值给数据, // 因为此时并未输入完成。 addEventListener(el, 'compositionstart', onCompositionStart) // 当用户从输入法中确定选中了一些数据完成输入后(如中文输入法常见的按空格确认输入的文字), // 设置 `composing=false`,在 onCompositionEnd 中手动触发 input 事件,完成数据的赋值。 addEventListener(el, 'compositionend', onCompositionEnd) // Safari < 10.2 & UIWebView doesn't fire compositionend when // switching focus before confirming composition choice // this also fixes the issue where some browsers e.g. iOS Chrome // fires "change" instead of "input" on autocomplete. addEventListener(el, 'change', onCompositionEnd) } } } function onCompositionStart(e: Event) { (e.target as any).composing = true } function onCompositionEnd(e: Event) { const target = e.target as any if (target.composing) { target.composing = false target.dispatchEvent(new Event('input')) } } const getModelAssigner = (vnode: VNode): AssignerFn => { const fn = vnode.props!['onUpdate:modelValue'] return isArray(fn) ? value => invokeArrayFns(fn, value) : fn }
代码有点多,但原理很简单:
- 通过自定义监听事件
addEventListener
来监听input
元素的input
或change
事件 - 当用户手动输入数据时执行对应的函数,并通过
el.value
获取input
的新值 - 调用
el._assign
(onUpdate:modelValue
属性对应的函数)方法v-model
绑定的值
而实现 DOM 到数据的单向流动,关键就在 onUpdate:modelValue
。借助 Vue 3 Template Explorer,我们可以查看其编译后生成的 render
函数,可以发现它做所的事情并没有什么神奇的地方,就是帮我们自动更新 v-model
上绑定的变量的值。
<input type="text" v-model="search"> 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", { type: "text", // `onUpdate:modelValue` 所做的事, // 就是自动帮我们更新 `v-model` 上绑定的变量的值。 "onUpdate:modelValue": $event => ((_ctx.search) = $event) }, null, 8 /* PROPS */, ["onUpdate:modelValue"])), [ [_vModelText, _ctx.search] ]) }
除此之外,还有对 lazy
的处理、trim
的处理、数字的处理、以及解决正在输入时文本被清空的问题。
关于 onCompositionStart
和 onCompositionEnd
两个方法的作用,详见 text added with IME to input that has v-model is gone when the view is updated #2302。
一句话总结:通过使用 addEventListener
来实现 DOM 到数据的单向流动。
最后是 beforeUpdate
的实现,如果数据的值和 DOM 的值不一致,则将数据更新到 DOM:
// packages/runtime-dom/src/directives/vModel.ts beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) { el._assign = getModelAssigner(vnode) // avoid clearing unresolved text. #2302 // 输入某些语言如中文,在没有输入完成时,在更新时会自动将已存在的文本清空,具体可见 issue#2302 if ((el as any).composing) return 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 } }
以上就是 text
类型的 input
元素双向绑定原理,当然 input
元素类型不止这个,还有诸如 radio
、checkbox
等类型,大家有兴趣的话可以自己去看,但是原理都是相同的,就是实现两个功能:数据到 DOM 的单向流动、DOM 到数据的单向流动。
本文来自博客园,作者:AshengTan,转载请注明原文链接:https://www.cnblogs.com/ashengtan/p/16157514.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异