双向数据绑定是使用 Vue 构建 JavaScript 表单的强大模式。
例如,假设您有一个 input
元素和一个 JavaScript 变量 value
。双向数据绑定意味着:
- 当用户键入
input
时,value
将被更新以匹配input
中的值。 - 更新
value
时,input
元素的内容将更新以匹配value
。
Vue 通过 v-model
属性支持双向数据绑定。例如:
<template>
<input v-model="value" />
<button @click="value = 'Hello Vue'">Reset</button>
</template>
<script>
export default {
data() {
return { value: 'Hello Vue' }
}
}
</script>
要实现一个双向绑定,我们需要了解以下几点。
Vue 的双向数据绑定(MVVM)由三个重要部分构成:
- Model 代表数据层,可定义修改数据和编写业务逻辑。
- View 代表视图层(UI 组件),负责将数据渲染成页面。
- ViewModel 负责监听数据层数据变化,控制视图层行为交互。简单讲,就是同步数据层和视图层的对象。ViewModel 通过双向绑定把 View 和 Model 层连接起来,且同步工作无需人为干涉,使开发人员只关注业务逻辑,无需频繁操作 DOM,不需关注数据状态的同步问题。
很像面试总结说法,大概了解即可。
重点理解 ViewModel,它的主要职责是:
- 数据变化更新视图
- 视图变化更新数据
以下是 Vue 官网提供的 MVVM 示例图:
通过 MVVM 示例图,我们可以清晰的看到,Vue ViewModel 通过 DOM 监听和指令来实现。
具体的说,Vue 内部在实现 ViewModel 层上,分为两部分:
- 监听器(Observer)— 对所有数据的属性进行监听
- 解析器(Compiler)— 对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
Vue 指令数据绑定有两种:
- 双向绑定的
v-model
- 单向绑定的
:value
而 Vue v-model
指令其实是属性 + 事件的语法糖。
简单的说,双向绑定由两个单向绑定组成:
- 模型 —> 视图数据绑定
- 视图 —> 模型事件绑定
通过 :value
实现了模型到视图的数据绑定,@event
实现了视图到模型的事件绑定:
<input :value="searchText" @input="searchText = $event.target.value" />
<!-- 等同于 -->
<input v-model="searchText" />
其他的表单元素绑定如下:
input
元素若是text / textarea
类型,使用value
属性和input
事件。input
元素若是radio / checkbox
类型,使用checked
属性和change
事件。select
元素使用value
属性和change
事件。
最后一点,就是实现响应式数据基础了。
Vue 2.x 基于 Object.defineProperty
实现响应式系统,而 Vue 3 使用 Proxy。
这里就有个问题,Vue 为什么从 Object.defineProperty
替换为 Proxy?
可以从优缺点入手。
替换掉 Object.defineProperty
肯定是它有一些缺点:
- 无法监听数组下标的变化,导致通过数组下标添加元素,无法实时响应
- 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择
Proxy 的优势就是为了解决 Object.defineProperty
的一些劣势,除此之外:
- Proxy 可以直接监听数组的变化。
- Proxy 有多达 13 种拦截方法,不限于
apply
、ownKeys
、deleteProperty
、has
等,这些是Object.defineProperty
不具备的。 - Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的。而
Object.defineProperty
只能遍历对象属性直接修改。 - Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是新标准的性能红利。
- 当然,Proxy 的劣势就是兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本(3.0)才能用 Proxy 重写。
关于 Proxy 和 Reflect 还有不认识的可以看看阮一峰老师的 Proxy 和 Reflect,也可以看看现代 JavaScript 教程的 Proxy 和 Reflect。
一个需要绑定的 input
标签和用于显示数据的 span
标签:
<input id="input" /> <span id="span"></span>
Vue 2.x 使用 ES5 Object.defineProperty
:
const input = document.getElementById('input')
const span = document.getElementById('span')
const obj = {}
// 数据劫持
Object.defineProperty(obj, 'text', {
get() {
console.log('获取数据')
},
set(value) {
console.log('数据更新')
span.textContent = value
this.value = value
}
})
// 监听
input.addEventListener('input', (e) => {
obj.text = e.target.value
})
Vue 3 使用 ES6 Proxy,它可以直接劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都比 Object.defineProperty
强。
const input = document.getElementById('input')
const span = document.getElementById('span')
const obj = {}
// 数据劫持
const ret = new Proxy(obj, {
get(target, key, receiver) {
console.log(target, key, receiver)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(target, key, value, receiver)
if (key === 'text') {
input.value = value
span.textContent = value
}
return Reflect.set(target, key, value, receiver)
}
})
// 监听
input.addEventListener('input', (e) => {
ret.text = e.target.value
})
以上是极简的双向数据绑定的示例。
更具体的实现,后面有空在写一写。