Vue响应式原理
Vue响应式原理
1. 什么是Vue响应
数据发生变化后,会重新对页面渲染,这就是Vue响应式
2. Vue响应过程
- 数据劫持 / 数据代理:侦测数据的变化
- 依赖收集:收集视图依赖了哪些数据
- 发布订阅者模式:数据变化时,自动“通知”需要更新的试图部分,并进行更新
3. 如何侦测数据变化
两种方法:
- Object.defineProperty:数据劫持
- ES6的Proxy:数据代理
3.1. Object.defineProperty 实现
Vue通过设定对象属性的 setter/getter
方法来监听数据的变化:
getter
收集依赖- 每个
setter
方法都是一个观察者
,在数据变更
时,通知订阅者
更新视图
代码如下:
function render () {
//set的时候会走这里,重新渲染
console.log('模拟视图渲染')
}
let data = {
name: 'gagaLab',
location: { x: 100, y: 100 }
}
observe(data)
定义核心函数 observe
function observe (obj) { // 我们来用它使对象变成可观察的
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
function defineReactive (obj, key, value) {
// 递归子属性
observe(value)
Object.defineProperty(obj, key, {
enumerable: true, //可枚举(可以遍历)
configurable: true, //可配置(比如可以删除)
get: function reactiveGetter () {
console.log('get', value) // 监听
return value
},
set: function reactiveSetter (newVal) {
observe(newVal) //如果赋值是一个对象,也要递归子属性
if (newVal !== value) {
console.log('set', newVal) // 监听
render()
value = newVal
}
}
})
}
}
改变data的属性,触发set;获取data的属性,触发get。
data.location = {
x: 1000,
y: 1000
} //打印 set {x: 1000,y: 1000} 模拟视图渲染
data.name //打印 get gagaLab
以上代码的主要作用:
observe这个函数传入一个 obj(需要被追踪变化的对象)
,通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive
处理,给每个属性加上set
和get
方法,以此来达到实现侦测对象变化。值得注意的是,observe 会进行递归调用。
如何侦测Vue中data的数据:
class Vue {
/* Vue构造类 */
constructor(options) {
this._data = options.data;
observer(this._data);
}
}
但是!上面的代码无法检测到对象属性的添加或删除
(如data.location.a=1,增加一个a属性)。
因为 Vue 通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改
,无法追踪新增属性和删除
属性。
-
如果要删除属性,可以用
vm.$delete
实现。 -
如果要新增属性
- 可以使用 Vue.set(location, a, 1) 方法向嵌套对象添加响应式属性;
- 也可以给这个对象重新赋值,比如data.location = {…data.location,a:1}
另外,Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写
📌 我的代码
// 监听数据变化
let obj = {
name: 'Jack'
};
Object.keys(obj).forEach(key => {
let value = obj[key];
Object.defineProperty(obj, key, {
set(newVal) {
console.log("我监听到了value发生变化");
value = newVal;
},
get() {
console.log("我获取到了value");
return value;
}
})
})
3.2. Proxy实现
Proxy 是 JavaScript 2015
的一个新特性。
Proxy 的代理
是针对整个对象
的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性,Proxy 只需要做一层代理
就可以监听同级结构
下的所有属性变化,当然对于深层结构
,递归还是需要进行的。此外Proxy支持代理数组
的变化。
这种方法,代码不仅精简,而且还是实现一套代码对对象和数组的侦测都适用。
不过Proxy兼容性不太好!
代码如下:
function render() {
console.log('模拟视图的更新')
}
let obj = {
name: '前端工匠',
age: { age: 100 },
arr: [1, 2, 3]
}
let handler = {
get(target, key) {
// 如果取的值是对象就再对这个对象进行数据劫持
if (typeof target[key] == 'object' && target[key] !== null) {
return new Proxy(target[key], handler)
}
return Reflect.get(target, key)
},
set(target, key, value) {
//key为length时,表示遍历完了最后一个属性
if (key === 'length') return true
render()
return Reflect.set(target, key, value)
}
}
let proxy = new Proxy(obj, handler)
proxy.age.name = 'gagaLab' // 支持新增属性
console.log(proxy.age.name) // 模拟视图的更新 浪里行舟
proxy.arr[0] = 'gagaLab' //支持数组的内容发生变化
console.log(proxy.arr) // 模拟视图的更新 ['gagaLab', 2, 3 ]
proxy.arr.length-- // 无效
4. 收集依赖
4.1. 为什么要收集依赖
我们之所以要观察数据,其目的在于当数据的属性发生变化时,可以通知那些曾经使用了该数据的地方。比如例子中,模板中使用了location 数据,当它发生变化时,要向使用了它的地方发送通知。
let globalData = {
text: '浪里行舟'
};
let test1 = new Vue({
template:
`<div>
<span>{{text}}</span>
<div>`,
data: globalData
});
let test2 = new Vue({
template:
`<div>
<span>{{text}}</span>
<div>`,
data: globalData
});
如果执行以下语句:
globalData.text = 'gagaLab';
就需要通知 test1
以及 test2
这两个Vue实例进行视图的更新
,我们只有通过收集依赖
才能知道哪些地方依赖我的数据,以及数据更新时派发更新
。那依赖收集是如何实现的?其中的核心思想就是“事件发布订阅模式”。
4.2. 如何收集依赖
4.2.1 订阅者Dep
为什么要引入Dep:
收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep。它用来收集依赖
、删除依赖
和向依赖发送
消息等。 于是我们先来实现一个订阅者 Dep 类
,用于解耦属性
的依赖收集和派发更新操作,说得具体点:它的主要作用是用来存放 Watcher 观察者
对象。我们可以把Watcher理解成一个中介
的角色,数据发生变化
时通知它,然后它再通知其他地方
。
Dep的简单实现:
class Dep {
constructor () {
/* 用来存放Watcher对象的数组 */
this.subs = [];
}
/* 在subs中添加一个Watcher对象 */
addSub (sub) {
this.subs.push(sub);
}
/* 通知所有Watcher对象更新视图 */
notify () {
this.subs.forEach((sub) => {
sub.update();
})
}
}
以上代码主要做了两件事:
- 用 addSub 方法可以在目前的
Dep
对象中增加一个Watcher
的订阅操作; - 用 notify 方法通知目前
Dep
对象的subs
中的所有Watcher
对象触发更新操作。 所以当需要依赖收集
的时候调用 addSub,当需要派发更新
的时候调用 notify。
调用:
let dp = new Dep()
dp.addSub(() => {//依赖收集的时候
console.log('emit here')
})
dp.notify()//派发更新的时候
4.2.2. 观察者 Watcher
Vue 中定义一个 Watcher 类来表示观察订阅依赖
。
为什么要引入Watcher:
当属性
发生变化后,我们要通知用到数据
的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理
这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。
依赖收集的目的:
将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。
形成如下所示的这样一个关系
Watcher的简单简单实现:
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
this.cb(this.value)
}
}
在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher,然后执行 update 函数。
4.3 依赖的本质
所谓的依赖
,其实就是Watcher
。
4.4 如何收集依赖
**一句话概括:**在getter
中收集依赖,在setter
中触发依赖。先收集依赖,即把用到该数据的地方收集起来
,然后等属性发生变化
时,把之前收集好的依赖循环触发
一遍就行了。
具体来说,当外界通过Watcher读取数据
时,便会触发getter
从而将Watcher添加到依赖中
,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。
5. 流程图
- 在 new Vue() 后, Vue 会调用_init 函数进行初始化,也就是init 过程,在 这个过程Data通过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行getter 函数,而在当被赋值的时候会执行 setter函数。
- 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。
- 在修改对象的值的时候,会触发对应的setter, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图。