学习完对象的响应式定义后,再看下数组的响应式定义,回到Observer的构造函数中,看下面的代码。
if (Array.isArray(value)) {
// ! 判断当前环境的对象中是否有 __proto__ 属性 (是否支持使用 __proto__)
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value); // ! 再次监听数组,监听一些嵌套的数组
} else {
// ...
}if代码块中定义了数组响应式,首先判断宿主环境中是否存在__proto__属性,这个属性是对象的隐式原型对象,只有在 IE 11 以上的浏览器才会支持。
如果宿主环境支持__proto__,调用protoAugment函数并传入数组类型的value和arrayMethods这两个值作为参数。
arrayMethods#
先不急着看protoAugment函数,先看下第二个参数arrayMethods是什么,在core/observer/array.js文件中。
const arrayProto = Array.prototype; // ! 数组原型
export const arrayMethods = Object.create(arrayProto); // ! 通过数组原型创建的对象arrayMethods是根据数组的原型对象创建传来的对象。但是还没完?继续看下面的代码。
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]; // ! 原生方法
def(arrayMethods, 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); // ! 监听新增的值,使其成为响应式
// notify change
ob.dep.notify(); // ! 手动触发依赖通知
return result;
});
});methodsToPatch是一些数组常用方法名(字符串类型)组成的数组,通过这些方法可以操作数组中元素的增加、删除或者移位。
再看下面的代码,遍历methodsToPatch,然后重写arrayMethods对象中的某些方法,这些方法就是在methodsToPatch里面的方法。
先看遍历函数中的第一行代码。
const original = arrayProto[method]; // ! 原生方法声明常量original存储数组原型的原生方法。
继续往下看
def(arrayMethods, method, function mutator(...args) {
// ...
}接下来调用def函数重写arrayMethods对象中的方法。
看下def的第三个参数val,它现在是一个函数,看这个函数第一行和最后一行的代码。
const result = original.apply(this, args); // ! 调用原生方法生成值,也是返回值
// ...
return result;传入参数并使用原生方法生成值,然后在最后返回这个值,这样就相当于继承了原生方法。
然后在中间处理了一些事情,现在看中间的代码。
const ob = this.__ob__; // ! this 指向数组本身
let inserted; // ! 数组新增的值, 数组类型
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2); // ! 获取传入参数的第三个参数以后的值所有值,即新增的值
break;
}
if (inserted) ob.observeArray(inserted); // ! 监听新增的值,使其成为响应式
// notify change
ob.dep.notify(); // ! 手动触发依赖通知首先声明常量ob存储this.__ob__的值,this指向数组本身。我们都知道在初始化data选项时,会使用observe函数监听data选项,然后在data里面添加一个__ob__属性,这个属性存储了Observer实例本身。
def(value, "__ob__", this);继续看下面的代码。,声明了一个inserted变量,这个变量存储着数组新增的值,它是一个数组。
再使用switch函数匹配不同方法增加的值,比如push和unshift增加的值,就是传入的参数,可以是一个参数,也可以是多个参数,这时直接赋值。
示例代码:
const arr = [];
arr.push(1, 2);
arr.unshift(5);
console.log(arr); // [ 5, 1, 2 ]
arr.splice(1, 0, 4, 5, 6);
console.log(arr); // [ 5, 4, 5, 6, 1, 2 ]但是splice方法比较特殊,通过这个方法新增的值是从第三个参数开始的。所以使用slice方法从第三个参数开始截取参数数组。
inserted = args.slice(2); // ! 获取传入参数的第三个参数以后的所有值,即新增的值所以inserted设置成一个数组是比较合理的,因为数组增加元素时,有可能一下子增加好几个元素。
新增元素后,还需要把增加的元素变成响应式数据,因为增加元素的是数组,不能自动使用Object.defineProperty定义元素,需要使用observeArray方法把它们变成响应式数据。
看下面的代码。
if (inserted) ob.observeArray(inserted); // ! 监听新增的值,使其成为响应式调用ob中的observeArray把他们变成响应式的。ob是Observer的实例对象,在前面我们知道类Observer有两个方法,前面已经学习了walk方法,现在讲解另外一个方法,就是刚才调用的``observeArray`方法,下面看这个方法的代码。
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}这个方法很简单,就是遍历传入的数组,然后通过observe函数监听数组中的每一个元素,使它们都成为响应式数据。
当然,最后还需要手动触发依赖。
ob.dep.notify(); // ! 手动通知,触发依赖注意这里不只是新增加值需要手动触发依赖,其他的操作比如删除元素、移动元素也是需要手动触发依赖的。因为数组类型的数据没法使用Object.defineProperty这个 API 来监听数据的变化,自动触发依赖。
protoAugment#
继续回到类Observer的构造函数中,如果宿主环境支持__proto__时,调用protoAugment方法并把``value和arrayMethods`作为参数传入。
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value); // ! 再次监听数组,监听一些嵌套的数组了解第二个参数arrayMethods对象后,现在可以参看protoAugment函数的代码。
function protoAugment(target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}这个方法接收两个参数target和src,也就是上面传入的value和arrayMethods,然后用__proto__把它们连接起来。
使用protoAugment函数把value和arrayMethods通过原型连接起来,这样数组类型的监听对象value也继承了arrayMethods对象的变异方法(重写的方法),在数组增加元素的时候也可以使增加的元素变成响应式数据,还能触发value中依赖的更新。
copyAugment#
接下来看下在不支持__proto__的宿主环境中,是如何处理数组类型的数据。
copyAugment(value, arrayMethods, arrayKeys);使用了copyAugment函数,这个函数比protoAugment多传入了一个参数,现在看下多出来的第三个参数arrayKeys是什么样子。
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);原来是获取arrayMethods对象中所有属于自身的属性名,即key值(不是原型链上的方法)。
它应该是下面这个样子。
const arrayMethods = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];现在看下copyAugment函数的代码。
function copyAugment(target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
def(target, key, src[key]);
}
}因为不能使用隐式原型对象__proto__连接target和src。
这里重定义的方法,遍历keys,在每次循环时,获取到key值,就是那些常用的数组方法。然后使用def函数重写target中的这些方法,让它们变成src对象,也就是传入的arrayMethods对象的变异方法。
这样就和上面的原型继承达到相同的目的,即数组类型的数据在修改数据时,也能触发依赖,然后数据更新,并且会把数组中新增的元素变成响应式数据。
接下来看最后一行代码。
this.observeArray(value); // ! 再次监听数组,监听一些嵌套的数组使用observeArray方法监听value值,主要是防止数组类型数据里面还存在着数组,也就是嵌套数组,把嵌套数组里面的元素也变成响应式数据。
dependArray#
把数组类型的数据变成响应式数据过程我们已经了解了,现在看下数组类型的数据是怎么收集依赖的。
在defineReactive函数定义getter的时候,有以下代码,在core/observer/index.js文件中。
if (Dep.target) {
dep.depend(); // ! 添加进订阅器,依赖收集
if (childOb) {
childOb.dep.depend(); // ! 依赖收集
if (Array.isArray(value)) {
dependArray(value); // ! 数组手动收集依赖
}
}
}判断下value的值是否是数组,如果是数组,调用dependArray函数收集依赖。
先看下dependArray函数的代码。
function dependArray(value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i];
e && e.__ob__ && e.__ob__.dep.depend(); // ! 依赖收集
if (Array.isArray(e)) {
dependArray(e); // !递归调用
}
}
}它的逻辑很简单,遍历数组的值,然后调用它的ob进行依赖收集,如果这个值还是一个数组,递归调用这个函数。
因为数组的结构和对象不同,Object.defineProperty这个属性有局限性,它只能定义对象key的值,而无法定义数组索引的值,也就是说无法给数组里面的元素添加getter属性。当读取数组中的值时,也就不会触发getter函数自动取收集依赖,所以需要手动收集依赖。
小结#
首先虽然我们可以给数组中添加__ob__属性,不能使用Object.defineProperty来定义数组中是索引值,因为Object.defineProperty的局限性,它只支持对象而不支持数组。Vue 3.0 中使用Proxy就不会发生这种情况。
也就是说数组里面的元素,没有属性描述符,也没有getter和setter,即不能自动收集依赖和触发依赖。既然不能自动,那就手动呗。
在getter中,使用dependArray函数手动收集数组的依赖。
然后把数组类型的属性通过原型链连接arrayMethods来继承arrayMethods重写的一些变异方法。或者通过重新定义的方法,把数组类型的属性一些方法替换成arrayMethods的变异方法。
这样数组使用这些方法时,就会执行变异方法中我们设置的逻辑,这相当于对象类型的key触发setter一样,数组中的元素也能触发依赖,更新视图,然后对于数组新增的元素,也会把它们变成响应式数据。
vmCount#
到这样已经完全讲解了Observer构造函数,然后我们再次回到observer函数中,这个函数还剩下最后一点代码。
// ! 对象是根实例数据对象时, vmCount++
if (asRootData && ob) {
ob.vmCount++;
}
return ob;还记得initData在初始化data选项时的最后一行代码吗?
// observe data
observe(data, true /* asRootData */); // ! 把 data 上的属性变成响应式的使用observe函数时传入了第二个参数asRootData,它的值为true,说明最开始初始化的data是根组件的数据。
然后我们发现在后面监听数据时,一般都不会传入第二个参数,它的值就是默认值undefined,转换成布尔值就是false,说明监听的数据不是根组件数据。
回到下面的代码。
if (asRootData && ob) {
ob.vmCount++;
}
return ob;只有在根组件且监听根组件数据时,ob.vmCount的值是递增的,而ob.vmCount在类Observer的构造函数中声明的初始值是0,所以根组件的数据ob.vmCount的值永远是大于0的,observe函数最后返回ob实例。
那么是不是可以这样理解,只要是根组件数据,那么它的__ob__中的vmCount就大于0。这样的话,就可以通过判断ob.vmCount的值是否大于0来确定组件是不是根组件,或者监听的数据是不是根组件数据了。
为什么要专门区分根组件数据和非根组件数据呢?它们之间有什么区别吗?
因为一开始在初始化根数据之前,根数据data是非响应式的,这时候如果使用Vue.set或者vm.$set为data选项里面的对象添加值是不可能成功的,因为Vue.set或者vm.$set只能为响应式对象添加属性。
只有当data数据被观察者依赖时,才能收集依赖到订阅器中,这样之后才能使用Vue.set或者vm.$set为data添加属性。