目录
- 如何追踪变化
- 拦截器
- 使用拦截器覆盖Array原型
- 将拦截器方法挂载到数组的属性上
- 如何收集依赖
- 依赖列表存在哪儿
- 收集依赖
- 在拦截器中获取Observer实例
- 向数组的依赖发送通知
- 侦测数组中元素的变化
- 侦测新增元素的变化
- 关于Array的问题
- 总结
如何追踪变化
Object的变化是靠setter来追踪的,只要一个数据发生了变化,一定会触发setter。
Array是通过push等数组原型的操作来改变数组的内容,因此只要在用户使用push操作数组的时候得到通知,就能实现同样目的。
可以利用一个拦截器覆盖Array.prototype,每当使用Array原型上的方法操作数组时,其实执行的都是拦截器中提供的方法。然后,在拦截器中使用原始Array的原型方法去操作数组。
拦截器
拦截器其实就是一个和Array.prototype一样的Object,里面包含的属性一模一样,只不过这个Object中某些可以改变数组自身内容的方法使我们处理过的。
Array原型中可以改变自身的方法有:push、pop、shift、unshift、splice、sort和reverse
// 拦截器
const changeSelf = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
const arrayProto = Array.prototype;
// arrayMethod的原型为arrayProto,即arrayMethod._proto===arrayProto
const arrayMethod = Object.create(arrayProto);
// 捕获数组原型上的方法,添加到arrayMethod上
changeSelf.forEach((method) => {
const original = arrayProto[method];
Object.defineProperty(arrayMethod, method, {
value: function mutator(...args) {
return original.apply(this, args);
},
enumerable: false,
configurable: true,
writable: true,
});
});
当使用push方法时,其实调用的是arrayMethod.push,而arrayMethod.push是函数mutator,也就是实际上执行的是mutator函数。
使用拦截器覆盖Array原型
有了拦截器,想让他生效,就需要去覆盖Array原型,但是直接覆盖会污染全局的Array,这并不是我们所希望的结果。
我们只希望拦截器只覆盖那些响应式数组的原型。
可以通过Observer,在Observer中使用拦截器覆盖那些即将被转换成响应式Array数据的原型就好了:
class Observer {
constructor(value) {
this.value = value;
if (Array.isArray(value)) {
value.__proto__ = arrayMethod;
} else {
this.walk(value);
}
}
}
将拦截器方法挂载到数组的属性上
并不是所有浏览器都支持__proto__,因此,需要处理不能使用__proto__的情况。
Vue的做法是直接将arrayMethod身上的方法设置到被侦测的数组上:
const hasproto = "__proto__" in {};
const arraykeys = Object.getOwnPropertyNames(arrayMethod);
class Observer {
constructor(value) {
this.value = value;
if (Array.isArray(value)) {
const augment = hasproto ? protoAugment : copyAugment;
augment(value, arrayMethod, arraykeys);
} else {
this.walk(value);
}
}
}
function protoAugment(target, src, keys) {
target.__proto__ = src;
}
function copyAugment(target, src, keys) {
for (let i = 0; i < keys.length; i++) {
def(target, keys[i], src[key[i]]);
}
}
如何收集依赖
前面我们创建了拦截器,我们之所以创建拦截器,本质上是为了得到一种能力,一种当数组的内容发生变化时得到通知的能力。
前面介绍Object时,是通知Dep中的依赖(watcher),数组也是如此,通知给Dep中的依赖。
而Object的依赖是如何收集的呢?
Object的依赖前面介绍过,是在defineReactive中的getter里使用Dep收集的,每个key都会有一个对应的Dep列表来存储依赖。
简单地说,就是在getter中收集依赖,依赖被存储在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(){
dep.depend();
return val;
},
set(newVal){
if(val===newVal){
return
}
dep.notify();
val = newVal
}
})
}
所以,Array是在getter中收集依赖,在拦截器中触发依赖。
依赖列表存在哪儿
Vue.js把Array的依赖存放在Observer中:
class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
if (Array.isArray(value)) {
const augment = hasproto ? protoAugment : copyAugment;
augment(value, arrayMethod, arraykeys);
} else {
this.walk(value);
}
}
}
数组在getter中收集依赖,在拦截器中触发依赖,因此它必须在getter和拦截器中都可以访问到。之所以把Dep保存在Observer实例上,是因为在getter中可以访问到Observer实例,同时在Array拦截器中也可以访问到Observer实例。
收集依赖
把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() {
dep.depend();
if(childOb){
childOb.dep.depend();
}
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
dep.notify();
val = newVal;
},
});
}
/*
为value创建一个Observer实例
如果创建成功,直接返回新创建的实例
如果value已经存在一个Observer实例,则直接返回它
*/
function observe(value, asRootData){
if(typeof value !== 'object'){
return
}
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
}else{
ob = new Observer(value);
}
return ob;
}
上述代码中,新增了observe函数,该函数的作用是创建一个响应式的Observer实例,如果value已经是响应式数据,无需再次创建,直接返回已经创建好的Observer实例即可,避免了重复侦测value变化的问题。
通过这种方式,我们可以实现在getter中将依赖收集到Observer实例的Dep中,即:为数组收集依赖。
在拦截器中获取Observer实例
因为Array拦截器是对原型的一种封装,所以可以在拦截器中访问到this(当前正在被操作的数组)。
而dep保存在Observer中,所以需要在this上读到Observer的实例:
// 工具函数
function def(obj,key,val,enumerable){
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
configurable: true,
writable: true
})
}
class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
def(value, '__ob__', this);
if (Array.isArray(value)) {
const augment = hasproto ? protoAugment : copyAugment;
augment(value, arrayMethod, arraykeys);
} else {
this.walk(value);
}
}
}
上述代码中def工具函数的作用是,在value上新增一个不可枚举的属性__ob__,这个属性的值就是当前Observer实例。
这样我们就可以通过数组数据的__ob__属性拿到Observer实例,从而拿到__ob__上的dep。
__ob__的作用不仅是为了在拦截器器中访问Observer实例,还可以用来标记当前value是否已经被Observer转换成响应式数据。
所有被侦测了变化的数据身上都会有一个__ob__属性来表示他们是响应式的,上一节的observe函数就是通过__ob__属性来判断:如果value是响应式的,就直接返回__ob__;如果不是,则使用new Observer来将数据转换成响应式的。
向数组的依赖发送通知
["push", "pop", "shift", "unshift", "splice", "sort", "reverse"].forEach(
(method) => {
const original = arrayProto[method];
def(arrayMethod, method, function mutator(...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
ob.dep.notify();
return result;
});
}
);
侦测数组中元素的变化
所有响应式数据的子数据都要侦测,不论是Object中的数据还是Array中的数据,如果用户使用了push往数组中新增了数据,那么这个元素的变化也需要侦测。
这里先介绍如何侦测所有数据子集的变化:
class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
def(value, "__ob__", this);
if (Array.isArray(value)) {
const augment = hasproto ? protoAugment : copyAugment;
augment(value, arrayMethod, arraykeys);
this.observeArray(value);
} else {
this.walk(value);
}
}
// 侦测数组的子数据
observeArray(items) {
for (let i = 0; i < items.length; i++) {
observe(items[i]);
}
}
// 侦测对象的所有属性
walk(obj) {
const keys = Object.keys(obj);
keys.forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
侦测新增元素的变化
- 获取新增元素
["push", "pop", "shift", "unshift", "splice", "sort", "reverse"].forEach(
(method) => {
const original = arrayProto[method];
// 向数组的依赖发送通知
def(arrayMethod, method, function mutator(...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
ob.dep.notify();
return result;
});
}
);
- 使用Observer侦测新增元素
["push", "pop", "shift", "unshift", "splice", "sort", "reverse"].forEach(
(method) => {
const original = arrayProto[method];
// 向数组的依赖发送通知
def(arrayMethod, method, function mutator(...args) {
const result = original.apply(this, 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 result;
});
}
);
关于Array的问题
- 修改数组中第一个元素的值时,无法侦测到数组的变化;
this.list[0] = 2
- 通过length清空数组的操作也无法侦测到数组的变化。
this.list.length = 0
总结
- Array是通过方法来改变内容的,所以我们通过创建拦截器去覆盖数组原型的方式来追踪变化。
- 为了不污染全局Array.prototype,在Observer里只针对那些需要侦测变化的数组使用__proto__来覆盖原型方法,对于不支持__proto__属性的浏览器,直接循环拦截器,把拦截器中的方法直接设置到数组身上来拦截Array.prototype上的原生方法。
- Array在getter中收集依赖,在拦截器中向依赖发消息,因此依赖保存在了Observer实例上。
- 对每个侦测了变化的数据都标上印记__ob__,并把this(Observer实例)保存在__ob__上,一方面为了标记数据是否被侦测了变化(保证一个数据只被侦测一次),另一方面可以通过数据取到__ob__,从而拿到Observer实例上保存的依赖。当拦截到数组发生变化时,向依赖发送通知。