前言
双向绑定机制是Vue中最重要的机制之一,甚至可以说是Vue框架的根基,它将数据与视图模板相分离,使得数据处理和页面渲染更为高效,同时它也是前端面试题中的常客,接下来让我们来了解什么是双向绑定以及其实现原理。
什么是双向绑定
vue的双向绑定,即数据与视图的响应式设计。具体表现为:View 的改变能实时让Model发生变化,而 Model 的变化也能实时更新 View。而单项数据绑定,所有数据只有一份,一旦数据变化,就去更新页面(只有data-->DOM,没有DOM-->data)。
总之,所谓双向绑定,指的是 Vue 实例中的 data 与其渲染的 DOM 元素的内容保持一致,无论谁被改变,另一方会相应的更新为相同的数据。
使用双向绑定(v-model)
Vue2
<template>
<div id="app">
<input type="text" v-model="message">
<p>{{message}}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: "Hello, Vue2!"
};
}
};
</script>
Vue3
<template>
<div id="app">
<input type="text" v-model="message">
<p>{{message}}</p>
</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const message= ref("Hello, Vue 3!");
return { message};
}
};
</script>
注意,Vue双向绑定的对象一定要是响应式的。
双向绑定的原理
实现模式
Vue双向数据绑定是通过数据劫持+发布订阅者模式来实现的。Vue 采用的是 MVVM 架构,实现 MVVM 主要包含两个方面,一是数据变化更新视图,二是试图变化更新数据。 在实现过程上来说,主要有四个模块:
- 监听器Observer:执行劫持监听的所有属性,如果属性发生变化了,就通知订阅者Watcher看是否需要更新。
- 订阅者Watcher:可以受到属性的变化通知并执行相应的函数,从而更新视图。
- 消息订阅器Dep:因为订阅者有很多个,所以需要一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理。
- 解析器Compile:可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
Vue2与Vue3绑定模式的实现差异
需要注意的是虽然Vue2与Vue3都采用了数据劫持+发布订阅者的模式,但二者的实现原理有所不同:
- Vue2是通过 ES5 提供的 Object-defineProperty() 方法来劫持(监听)各属性的 getter、setter,并在当监听的属性发生变动时通知订阅者,是否需要更新,若更新就会执行对应的更新函数。
- Vue3加入ES2015中新增的 Proxy() 代替了原本的 Object.defineProperty()。
Vue3为什么弃用了ObjectdefineProperty选择了Proxy
- Proxy() 可以拦截数组和对象的变化。而 Object.defineProperty() 只能拦截对象属性的变化。
- 相较于 Object.defineProperty() 劫持某个属性,Proxy() 则更彻底,不在局限某个属性,而是直接对整个对象进行代理。
- Proxy能够监听到对象属性的增加、删除。
- Object.defineProperty() 不能对 ES6 新产生的 Map 、Set 这样的数据结构进行监听。
- Object.defineProperty() 无法监控到数组下标的变化,导致通过数组下标添加元素不能实时响应。
手写 Object.defineProperty() 双向数据绑定
<div>
展示:<h1></h1>
输入: <input type="text">
</div>
<script>
// 创建definePropertyFn来挟持数据
function definePropertyFn() {
let obj = {}
let val = null
Object.defineProperties(obj, {
val: {
get() {
return val
},
set(newV) {
val = newV
// 数据控制视图 将更改的数据赋值给h1
document.querySelector('h1').innerText = newV
console.log('调用了set,获取:' + newV, val);
}
}
})
return obj
}
let newObj = definePropertyFn()
document.querySelector('h1').innerText = newObj.val // 调用了get,执行数据渲染视图
document.querySelector('input').value = newObj.val // 调用了get,执行数据渲染视图
// 下面监听视图 input 标签,标签一变动,将最新数据获取调用set,赋值给val,并且赋值给h1
document.querySelector('input').addEventListener('input', function () {
newObj.val = document.querySelector('input').value
})
</script>