目录
1 如何追踪变化
es6之前,js没有提供元编程的能力,页没有提供可以拦截原型方法的能力,但是可以用自定义的方法去覆盖原生的原型方法。
可以用一个拦截器覆盖Array.prototype。之后,每当使用Array原型上的方法操作数组时,其实执行的都是拦截器中提供的方法,比如push方法。然后,在拦截器中使用原生Array的原型方法去操作数组。
2 拦截器
拦截器其实就是一个和Array.prototype一样的Object,里面包含的属性一模一样,只不过这个Object中某些可以改变数组自身内容的方法是我们处理过的。
Array原型中可以改变数组自身内容的方法有7个,分别是push、pop、shift、unshift、splice、sort、reverse;
const arrayProto = Array.prototype;
// 使用arrayMethods去覆盖Array.prototype
export const arrayMethods = Object.create(arrayProto);
[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach(
(method) => {
// 缓存原始方法
const original = arrayProto[method];
/**
* 在arrayMethods上使用Object.defineProperty方法将那些可以改变数组自身
* 内容的方法进行封装
*/
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args) {
/**
* 当使用push方法时,其实带哦用的是arrayMethods.push,而arrayMethods.push是函数mutator,
* 实际上执行的是mutator函数。
* 在mutator中执行original来做它应该做的事,比如push的功能。
*/
return original.apply(this, args);
},
enumerable: false,
writable: true,
configurable: true,
});
}
);
3 使用拦截器覆盖Array原型
有了拦截器之后,想要它生效,需要使用它去覆盖Array.prototype。但是又不能直接覆盖,会污染全局的Array。我们只希望拦截器只覆盖响应式数组的原型。
export class Observer {
constructor(value) {
this.value = value;
if (Array.isArray(value)) {
/**
* 它的作用是将拦截器(加工后具备拦截功能的arrayMethods)赋值给value.__proto__,
* 通过__proto__可以巧妙的实现覆盖value原型的功能
*/
value.__proto__ = arrayMethods; // 新增
} else {
this.walk(value);
}
}
}
4 将拦截器方法挂载到数组的属性上
如果不能使用__proto__,就直接将arrayMethods身上的这些方法设置到被侦测的数组上;
import { arrayMethods } from "./array";
// __proto__是否可用
const hasProto = "__proto__" in {};
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
export class Observer {
constructor(value) {
this.value = value;
if (Array.isArray(value)) {
// 修改
const augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
} else {
this.walk(value);
}
// ....
}
}
function protoAugment(target, src, keys) {
// 覆盖原型
target.__proto__ = src;
}
function copyAugment(target, src, keys) {
// 将已经加工了拦截操作的原型方法直接添加到value的属性中
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
def(target, key, src[key]);
}
}
5 如何收集依赖
Object的依赖是在defineReactive中的getter里使用Dep收集的,每个key都会有一个对应的Dep列表来存储依赖。
Array的依赖和Object一样,也在defineReactive中收集:
function defineReactive(data, key, val) {
if (typeof val === "object") new Observer(val);
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend();
// 这里收集Array的依赖
return val;
},
set: function () {
if (val === newVal) {
return;
}
dep.notify();
val = newVal;
},
});
}
所以,Array在getter中收集依赖,在拦截器中触发依赖。
6 依赖列表存在哪儿
vue.js把Array的依赖存放在Observer中:
export class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep(); // 新增dep
if (Array.isArray(value)) {
// ....
}
// ...
}
}
为什么数组的dep(依赖)要保存在Observer实例上呢?
前面介绍了数组在getter中收集依赖,在拦截器中触发依赖,所以这个依赖保存的位置很关键,它必须在getter和拦截器中都可以访问到。
我们之所以将依赖保存在Observer实例上,是因为在getter中可以访问到Observer实例,同时在Array拦截器中也可以访问到Observer实例。
7 收集依赖
将Dep实例保存在Observer的属性上之后,我们可以把getter中像下面这样访问并收集依赖:
function defineReactive(data, key, val) {
let childOb = observe(val); // 修改
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend();
// 新增
if (childOb) {
childOb.dep.depend();
}
return val;
},
set: function () {
if (val === newVal) {
return;
}
dep.notify();
val = newVal;
},
});
}
/**
* 尝试为value创建一个Observer实例,
* 如果创建成功,直接返回新创建的Observer实例,
* 如果value已经存在一个Observer实例,则直接返回它
*/
export function observe(value, asRootData) {
// 在defineReactive函数中调用了observe,它把val当作参数传了进去并拿到一个返回值,那就是Observer实例
if (!isObject(value)) {
return;
}
let ob;
// 如果value已经是响应式数据,不需要再次创建Observer实例,
// 直接返回已经创建的Observer实例即可,避免了重复侦测value变化的问题
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else {
ob = new Observer(value);
}
return ob;
}
通过observe得到了数组的Observer实例(childOb),最后通过childOb的dep执行depend方法来收集依赖。
8 在拦截器中获取Observer实例
因为Array拦截器是对原型的一种封装,所以可以在拦截器中访问到this(当前正在被操作的数组)。
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true,
});
}
export class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
// 在value上新增一个不可枚举的属性__ob__,这个属性的值就是当前Observer的实例
def(value, "__ob__", this); //新增
if (Array.isArray(value)) {
// ....
}
// ....
}
}
这样我们就可以通过数组数据的__ob__属性拿到Observer实例,然后就可以拿到__ob__上的dep了。
当然,__ob__的作用不仅仅是为了在拦截器中访问Observer实例那么简单,还可以用来标识当前value是否已经被Observer转换成了响应式数据。
也就是说,所有被侦测了变化的数据身上都会有一个__ob__属性来表示它们是响应式的。
当value身上被标记了__ob__之后,就可以通过value.__ob__来访问Observer实例。如果是Array拦截器,因为拦截器是原型方法,所以可以直接通过this.__ob__来访问Observer实例。具体实现如下:
[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach(
(method) => {
const original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args) {
const ob = this.__ob__; // 新增
return original.apply(this, args);
},
enumerable: false,
writable: true,
configurable: true,
});
}
);
9 向数组的依赖发送通知
前面介绍了如何在拦截器中访问Observer实例,所以这里只需要在Observer实例中拿到dep属性,就可以直接发送通知了:
[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach(
(method) => {
const original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args) {
const ob = this.__ob__;
ob.dep.notify(); // 向依赖发送消息
return original.apply(this, args);
},
enumerable: false,
writable: true,
configurable: true,
});
}
);
10 侦测数组中元素的变化
其中数组中保存了一些元素,它们的变化也是需要侦测的。也就是说,所有响应式数据的组数据都要侦测,不论是Object中的数据还是Array中的数据(数组中的每一项都需要转换成响应式的)。
所有Observer类不光要处理Object类型,还要处理Array类型。
export class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
def(value, "__ob__", this);
// 新增
if (Array.isArray(value)) {
this.observeArray(value);
} else {
this.walk(value);
}
// ....
}
// 侦测Array中的每一项,执行observe函数来侦测变化
observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
// ...
}
11 侦测新增元素的变化
如果是push、unshift、splice新增数组元素,同时也要将内容转换成响应式的,拦截器中对数组方法的类型进行判断,然后使用Obverser来侦测它们就行。
[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach(
(method) => {
const original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args) {
const ob = this.__ob__;
// 新增
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted); //新增
ob.dep.notify();
return original.apply(this, args);
},
enumerable: false,
writable: true,
configurable: true,
});
}
);
12 关于Array的问题
this.list[0] = 2 或this.list.length = 0 都是无法监听到的,不会触发re-render或watch。
13 总结
Array跟踪变化是通过创建拦截器去覆盖数组原型的方法来跟踪。
为了不污染全局的Array.prototype,我们在Observer中只针对需要侦测变化的数组使用__proto__来覆盖原型方法。
Array和Object收集依赖的方式是一样的,都是在getter中收集。但是由于使用依赖位置的不同,数组要在拦截器中向依赖发消息,所以依赖不能像Object那样保存在defineReactive中,而是把依赖保存在了Observer实例上。
在Observer中,每个侦测了变化的数据都标上了__ob__,并把this(Observer实例)保存在__ob__上。主要两个作用,一是为了标记数据是否被侦测了变化,二是方便通过数据取到__ob__,从而拿到Observer实例上保存的依赖。
除了侦测数组自己的变化外,数组中元素发生的变化也要侦测。在Observer中判断如果当前被侦测的数据是数组,则调用observeArray方法将数组中的每一项都转换成响应式的并侦测变化。
除了侦测已有数据以外,当用户使用push等方法添加新数据时,新增的数据也要进行变化侦测。在拦截器方法中,判断如果是push、unshift、splice方法,则从参数中将新增数据提取出来,然后使用observeArray对新增数据进行变化侦测。
注:本文章来自于《深入浅出vue.js》(人民邮电出版社)阅读后的笔记整理