95.Object的变化侦测
Opened this issue · 0 comments
Vue.js 是如何追踪 Object 的变化的?
Vue.js 会把对象作为参数,传递给 Observer 的构造函数,遍历所有属性,调用 defineReactive 把每个属性都转换成 getter/setter;
如果子属性也是对象,继续递归;
在 defineReactive 中,新建一个 dep 用来收集依赖,当某处访问到某个状态时,会触发 getter 收集依赖,之后状态修改时会触发 setter,对应的 dep 会通知收集的所有依赖 update;
另外,触发 setter 时,如果新的值是个对象,同样会使用 Observer 转换成响应式对象。
Object 的问题
我们观察下 Observer 的实现会发现,只有修改属性的操作会触发 setter 通知依赖更新,新增属性不会触发 setter 函数,因为新增的属性一开始不存在,也就没有 getter/setter。
Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。 ——Vue.js 官网
好在,官方给我们提供了 vm.$set 和 vm.$delete,可以实现追踪新增和删除属性。这两个 API
Vue.js 最独特的特性之一是看起来并不显眼的响应式系统。
从状态生成 DOM,再输出到用户界面显示的一整套流程叫做渲染,应用在进行时会不断地进行重新渲染。而响应式系统赋予框架重新渲染的能力,其重要组成部分就是 变化侦测。没有它,就没有重新渲染。框架在运行时,视图也无法随着状态的变化而变化。
简单来说,变化侦测的作用是侦测数据的变化。当数据变化时,会通知视图进行相应的更新。
Vue.js 中 Object 和 Array 的变化侦测采用不同的处理方式。
什么是变化侦测
通常,在运行时应用内部的状态会不断发生变化,此时需要不停地重新渲染。这是如何确定状态中发生了什么变化?
变化侦测就是用来解决这个问题的,它分为两种方式,一种是 “推”模型,一种是 “拉”模型。提到这两个词,想必一些同学就会感觉很熟悉。没错,就是观察者模式。
Angular 和 React 中的变化侦测都属于“拉”。 当状态发生变化时,框架并不知道具体是哪个状态变化了,所以会进行一个暴力对比来找出哪些 DOM 节点需要重新渲染。在 Angular 中是脏检查,在 React 中是虚拟 DOM。
Vue.js 的变化侦测属于“推”。 当状态发生变化时, Vue.js 能知道是哪些状态发生变化了,因此它可以进行更细粒度的更新。但是它也有一定的代价,因为粒度越细,每个状态所绑定的依赖就越多,依赖追踪在内存上的开销就会越大。因此,从 Vue.js 2.0 开始,引入了虚拟 DOM,将粒度调整为中等粒度,即一个状态所绑定的依赖不再是具体的 DOM 节点,而是一个组件。
如何追踪变化、收集依赖
有两种方法可以侦测到变化,Object.defineProperty 和 Proxy 。
function defineReative(data, key,val){
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function(){
return val;
},
set: function (newVal) {
if(val === newVal) return;
val = newVal;
}
})
}
封装好之后,每当从 data 的 key 中读取数据时,get 函数被触发,每当往 data 的 key 中设置数据时,set 函数被触发。
所以,我们可以在 getter 中收集依赖,在 setter 中触发依赖。
依赖收集在哪里
我们已经明确了目标,要在 getter 中收集依赖,那么应该把依赖收集到哪里呢?
我们先假设依赖是个函数,保存在全局变量上,如 window.target 上。我们把之前的 defineReactive 改造以下:
function defineReative(data, key, val) {
let dep = []; // 新增
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.push(window.target); // 新增
return val;
},
set: function (newVal) {
if (val === newVal) return;
val = newVal;
// 新增
dep.forEach(function (w) {
w(newVal, val);
});
},
});
}
我们新增了数组 dep,用来存储收集的依赖。当 set 函数被触发时,会依次触发收集的依赖。接下来,我们把依赖收集的相关代码封装成一个 Dep 类,用来收集依赖、删除依赖和向依赖发送通知等。
export default class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub);
}
}
removeSub(sub) {
remove(this.subs, sub);
}
depend() {
if (window.target) {
this.addSub(window.target);
}
}
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1);
}
}
}
然后,我们再改造一下 defineReactive:
function defineReative(data, key, val) {
let dep = new Dep(); // 修改
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend(); // 修改
return val;
},
set: function (newVal) {
if (val === newVal) return;
val = newVal;
// 修改
dep.notify();
},
});
}
现在,依赖被收集到 Dep 实例中。
依赖是谁
当状态发生变化时,需要通知谁,谁就是依赖。在上面的代码中,window.target 就是依赖。
由于依赖会一直观察着状态的变化,所以我们就把依赖叫做 Watcher。
Watcher 是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
我们先看一个 Watcher 的经典使用方式:
vm.$watch('a.b.c', function(newVal, oldVal){
// ...
}
这段代码表示当 data.a.b.c 发生变化时,就触发后面的函数。
那么,思考一下它是如何实现的?我们新建一个 Watcher 类来模拟一下:
export default class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
// 执行 this.getter(),就可以读取 data.a.b.c 的数据
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.cb = cb;
this.value = this.get();
}
get() {
window.target = this;
let value = this.getter.call(this.vm, this.vm);
window.target = undefined;
return value;
}
update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}
先把 window.target 设置成 this,也就是 watcher 实例,这时候读取 data.a.b.c 的值时,会触发 getter 进行依赖收集(回顾一下 defineReactive),把当前的 watcher 实例主动添加到 data.a.b.c 的 Dep 实例中。
每当 data.a.b.c 的值发生变化,触发 setter,对应的 Dep 调用 notify 方法,通知收集所有依赖,依次触发 update 方法。
我们再看一下 parsePath 是如何实现的:
export const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
将 path 用 . 分割成数组,然后循环获取。
递归侦测所有的 key
上面我们已经实现侦测单个key,下面我们要开始侦测所有的 key 了。我们新建一个 Observer 类, 把一个对象的所有属性都转换成 getter/setter:
export default class Observer {
constructor(value) {
this.value = value;
if(typeof value === 'object'){
this.walk(value); // 把对象的所有属性都转成 getter/setter
}
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
// 侦测单个 key 的变化
function defineReative(data, key, val) {
let dep = new Dep();
new Observer(val); // 新增, 如果子属性是对象,递归遍历子属性,同样转成 getter/setter
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend();
return val;
},
set: function (newVal) {
if (val === newVal) return;
val = newVal;
new Observer(newVal) // 新修改的值如果也是对象,同样转成 getter/setter
dep.notify();
},
});
}
上述代码,我们定义了 Observer 类,用来将对象转换成可被侦测的 object。只有对象才会调用 walk 方法来遍历转换。
我们在 defineReactive 函数中新增了一行 new Observer(val),如果对象的子属性也是对象,递归遍历转换成 getter/setter。我们还在 setter 中 新增了一行 new Observer(newVal),当新设置的值是个对象时,也会把这个对象转成响应式的 object。