Vue双向绑定:原理篇(详细)
文章目录
前言
提起Vue的双向绑定或数据响应式,很多人都知道是数据劫持和发布者-订阅者模式,这里具体分析一下这两部分具体是怎么实现。
(最近看了相关资料,对原来不足的地方进行修改完善,还增加了更新的粒度和Vue3的Proxy内容)
什么是响应式
如果经常使用Vue
或React
等框架开发,对响应式更新这个词并不陌生,简单来说就是视图会自动更新。
- 原生JS实现就需要先找到DOM,再修改DOM
比如
const clockDom = document.getElementById
clockDom.innerText = '修改文本内容'
- 现在使用响应式框架,修改数据的时候,我们不需要关注DOM。在Vue中,this.xxx就可以实现页面数据更新。我们从数据劫持开始了解。
数据劫持
- 数据劫持其实就是数据响应式基础,当获取数据或者修改数据的时候,能够被我们知道,然后触发响应操作,在Vue2中是通过Object.defineProperty()实现的。
比如,下面这个对象
let person = {
name:'tom',
age:15
}
- 我们可以
person.name
获取到tom
,但是我想在获取到tom
的时候,还要进行其他操作,就要使用Object.defineProperty()
Object.defineProperty(person,'name',{
get(){
console.log('name属性被读取了...');
},
set(newVal){
console.log('name属性被修改了...');
}
})
-
当
访问name属性
的时候,会调用get方法
,而修改name属性
的时候,会调用set方法
,可以去执行相应的操作。 -
但是,这个时候访问被拦截了,我们获取不到name的属性值,所以需要在get方法里面return一个值,上面代码修改如下:
let person = {}
let val = 'tom'
Object.defineProperty(person,'name',{
get(){
console.log('name属性被读取了...');
return val;
},
set(newVal){
console.log('name属性被修改了...');
val = newVal;
}
})
- 因为属性值可以由对象直接提供,不会单独声明,所以传入对象的时候,可以传入键和值。所以将val变量和
defineProperty
方法提取到一个函数中,就形成defineReactive
函数
function defineReactive(obj, key, val) { // 这里相当于let val= val(传入的参数)
Object.defineProperty(obj, key, {
get() {
console.log(`${key}属性被读取了...`);
return val;
},
set(newVal) {
console.log(`${key}属性被修改了...`);
val = newVal;
}
})
}
至此,就完成了简单的数据劫持
发布者-订阅者模式
模式简介
-
发布者和订阅者是互相不知道对方的存在的,发布者只需要把消息发送到订阅器里面,订阅者只管接受自己需要订阅的内容。
-
主要有三个概念:发布者、订阅器、订阅者
发布者 Observer
Observe
r就是进行数据劫持,内部包含了defineReactive()
函数。每次数据读或写时,我们能感知到数据被读取了或数据被改写了。要使数据变得“可观测”。
订阅器 dep
- 收集依赖,内部维护了一个数组,用来记录该数据的所有Watcher,一旦数据发生变化就会发布通知所有Watcher
订阅者 Watcher
- 作为依赖,会被dep收集。其实是个中介角色,数据发生变化时通知它,然后它去通知其他地方。
了解到这里,可能还会有些疑问:
-
依赖是什么?怎么产生?这就需要知道解析器 Compile,它会对模板进行解产生Watcher。
-
下面介绍一下双向绑定的整体流程,有一个更直观的了解。
整体流程
初始化data
- 首先要知道每个组件都是一个Vue实例,也就是new Vue(),然后将data等数据传入进去。
new Vue({
el: '#app', // 挂载点
data: { // 状态
},
methods: { // 方法
},
});
- 在Cass Vue中,可以在constructor中获取到data
constructor(options) {
this.$el = options.el //获取挂载点
this.$data = options.data
}
-
然后对
data.xx
的一级属性进行劫持,方法是直接遍历data的key,使用Object.defineProperty方法
对每个属性都进行劫持,返回对应的值data[key]
。 -
这也是为什么可以在Vue实例中直接通过
this.xxx
访问到data
中的数据的原因。 -
使用过React就知道,React使用
setdata()
方法才能修改数据。
data变为响应式数据
-
data对象实例化一个Observer实例,绑定在data的
ob
属性上面,防止重复绑定 -
Observer实例中创建一个dep实例,用于收集依赖
-
Observer内部有
Object.definedpropty
,对属性进行劫持,修改成getter
、setter
方法,用于依赖收集和派发更新 -
如果data中包含数组,Vue重写了数组的7种原生方法,实现响应式
-
如果data为多级对象,需要深度监听,递归data对象进行监听,data值更新的时候也需要进行判断深度监听
解析模板
-
对节点和Vue指令进行编译
-
编译过程中如果遇到
{{}}
或v-bind
、v-model
等指令使用到的时候,实例化Wacther(模板解析也另一大块内容,后面有机会再详细分析)
收集依赖
- 编译过程中当data中的某个属性被读时(模板中使用了data数据),get 方法会被调用, 该属性的dep实例会收集该属性的Watcher,放置到dep维护的数组中。
至此,修改data中的数据,就能够影响模板中的数据
数据变化—视图更新
- 修改data数据,Observer实例就会触发set方法,然后调用Dep 的
notify
方法,notify
方法中又去调用所有依赖该属性的 Watcher 的updater
方法,进行视图更新
视图更新—数据变化
-
上面的流程主要是数据变化更新视图,要实现双向绑定,还需要进行事件监听,也就是注册监听用户对视图的修改事件,触发修改data数据的方法
-
这也是
v-model
的实现双向绑定的原理
更新的粒度
更细的粒度更新
-
假如有一个状态绑定着好多依赖,每个依赖表示一个具体的DOM节点,那么当这个状态发生变化时,向这个状态的所有依赖发送通知,让它们进行DOM更新操作。
-
但是这样的代价是:粒度越细,每个状态所绑定的依赖就越多,依赖追踪的开销就越大。
中等粒度更新
- 从
Vue2.0
开始,它引入了虚拟DOM
,将粒度调整为中等粒度
,即一个状态所绑定的依赖不再是具体的DOM节点,而是一个组件。 - 这样状态变化后,会通知到组件,组件内部再使用虚拟DOM进行对比后进行重新渲染。
- 这可以大大降低依赖数量,从而降低依赖追踪所消耗的内存。
Vue3的Proxy数据劫持
- 从前面的内容可以看出
Vue2
的defineProperty
方法和重写数组方法的形式存在很多不足,针对这些问题,Vue3使用Proxy
来代替defineProperty
进行数据劫持。
理解Proxy
Proxy
是ES6新增的类,这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。Proxy
可以理解为:在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
特点
-
能监听对象和数组
-
新增的属性也能够被拦截,多层对象需要递归处理,对每一层对象进行代理
-
除了能拦截
访问
和修改
操作之外,还能拦截in操作符
和delete
操作符
// 不需要关心是哪个属性
new Proxy(data, {
get(key) { },
set(key, value) { },
})
// Vue2
Object.defineProperty(data, 'count', {
get() {},
set() {},
})
与defineProperty
的相同点就是对操作进行拦截,不同点是需要关心是哪个属性。
兼容性问题
- 但是
Proxy
是不能通过babel
转译的,因为在ES5中完全没有一种语法可以模拟出Proxy
的特性。因此Vue3.x
版本没有办法兼任一些低版本浏览器。
总结
-
双向绑定:就是数据变化更新视图,视图变化更新数据
-
数据响应式:通过对数据的访问和修改进行劫持,然后进行相应的操作,是实现双向绑定的基础。
-
Vue2数据劫持的缺点
监听对象:
Object.defineProperty
- 只能监听对象,这个对象不是指引用类型,所以不包括数组
- 不能够对新增属性进行监听
- 不能监听数组内部的元素
监听数组:Vue2重写了部分数组方法去实现,这部分和
Object.defineProperty
就没有关系了。-
直接通过索引修改数组无法触发更新
-
最好用**
splice
**方法对数组进行增删操作,因为splice在vue中重写了
新增属性:
$set
方法为对象的新增属性并进行拦截,如果是数组,$set
内部调用splice方法删除属性:
$delete
删除数据中的某个属性,并且能够侦测到数据的变化。 -
Vue3数据劫持使用Proxy
对比Vue2的拦截方式更加全面,避免了需要考虑使用
$set
之类等情况 -
发布者-订阅者模式
Observer:发布者,内部对数据访问和修改进行拦截,发送通知给Dep
Dep:订阅器,收集Watcher和通知Watcher
Watcher:订阅者,通知视图更新
这一篇文章主要从原理方面进行说明,在这个基础上可以看我下一篇实现双向绑定的文章。
Vue双向绑定:实现篇
参考