Vue响应式原理
一、什么是Vue响应式
数据发生变化后,会重新对页面渲染,这就是Vue响应式。
完成这个过程,我们需要:
- 数据劫持 / 数据代理(侦测数据的变化)
- 依赖收集(收集视图依赖了哪些数据)
- 发布订阅模式(数据变化时,自动“通知”需要更新的视图部分,并进行更新)
二、实现Vue响应式
1.数据劫持/数据代理
1.1 方法1.Object.defineProperty
Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。
Vue 通过 Object.defineProperty来将对象的key转换成 getter/setter的形式来追踪变化,但 getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。如果是删除属性,我们可以用 vm.$delete实现,那如果是新增属性,该怎么办呢?
(1)可以使用 Vue.set(location,a,1) 方法向嵌套对象添加响应式属性
(2)也可以给这个对象重新赋值,比如 data.location={…data.location,a:1}
1.2 方法2.Proxy
ES6提供了元编程的能力,所以有能力拦截,Vue3.0可能会用ES6中Proxy 作为实现数据代理的主要方式。
Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性, Proxy 只需要做一层代理就可以监听同级结构下的所有属性变化,当然对于深层结构,递归还是需要进行的。此外 Proxy支持代理数组的变化。但是 Proxy兼容性不太好!
2.依赖收集
依赖收集的核心思想是“事件发布订阅模式”。
2.1Dep订阅者
收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep。它用来收集依赖、删除依赖和向依赖发送消息等。订阅者 Dep 类,主要作用是用来存放 Watcher 观察者对象。我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
Dep的简单实现:
- 用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
- 用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。
- 当需要依赖收集的时候调用 addSub,当需要派发更新的时候调用 notify
2.2Watcher观察者
Vue 中定义一个 Watcher 类来表示观察订阅依赖。
为啥引入Watcher当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。
依赖收集的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。
Watcher的简单实现
在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher ,然后执行 update 函数。
3.代码实现
const obj = {}
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get() {
console.log('试图读取obj的a属性')
},
set(newValue) {
console.log('试图改变obj的a属性')
//属性变化时进行对Watcher实例进行通知
dep.notify()
}
})
})
//发布者
class Dep{
constructor(){
this.subs = []
}
addSub(watch){
this.subs.push(watch)
}
//属性变化发送通知函数
notify(){
//接受到通知后 调用update函数进行视图的更新
this.subs.forEach(item => {
item.update()
})
}
}
//订阅者
class Watcher {
constructor(name){
this.name =name
}
update(){
console.log(this.name+'发生update');
}
}
//模拟创建一个发布者
const dep = new Dep()
//模拟创建一个订阅者
const w1 = new Watcher('第一个实例')
//将订阅者添加到发布者中subs的数组里进行管理
dep.addSub(w1)
const w2 = new Watcher('第二个实例')
dep.addSub(w2)
const w3 = new Watcher('第三个实例')
dep.addSub(w3)
三、总结
如何收集依赖,总结起来就一句话,在getter中收集依赖,在setter中触发依赖。先收集依赖,即把用到该数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就行了。
具体来说,当外界通过Watcher读取数据时,便会触发getter从而将Watcher添加到依赖中,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。