1.为什么Array的侦测要和Object的侦测区分实现呢?
- Object是通过getter、setter实现的侦测,但数组中使用push等方法改变数组时,不会触发getter、setter
- 在数组中的元素如果是个对象,也要对他们的子属性进行监听。
- 在数组新增一个对象元素时,也要对新增的元素进行监听。
虽然Array的原型最终还是Object,但是正是由于Array的这些特性,导致了对Object的那一套侦测方式无法直接使用在数组Array中。
2.Array对象中包含了什么?
想要对Array进行监听,就需要了解Array里面到底有什么。
我们直接打印数组对象的原型
console.log(Array.prototype)
可以看到Array中的一些属性
可以看到其中包含了push concat等方法。
3.监听Array对象的大致思路
从上可以得出,可以通过覆盖Array对象中的原生方法push等,实现一些我们想要的操作。
或者说是直接定义一个拦截器,覆盖数组原型 Array.prototype
4.拦截器的实现思路
我们需要实现的拦截器,就相当于是把数组的原型对象Array.prototype复制出来, 然后将副本原型进行修改,并让数组的实例对象的原型指向该副本原型。
整理后发现,Array中有7个可以改变自身的方法:push、pop、shift、unshift、splice、sort、reverse。
所以只需要先重写这7个方法即可。
我们可以定义如下的拦截方式:
// 得到原生数组原型
const arrayProto = Array.prototype
// 相当于复制数组原型对象。
const arrayMethods = Object.create(arrayProto);
['push','pop','shift','unshift','splice','sort','reverse'].forEach(function (method) {
// 取出原生数组原型中的方法
const original = arrayProto[method]
// 监听副本中对应的方法
Object.defineProperty(arrayMethods, method, {
value: function mutator (...args) {
// 拦截!处理完自己的逻辑后,调用原生的方法
return original.apply(this, args)
},
enumerable: false,
writable: true,
configurable: true
})
})
5.拦截器怎么覆盖Array的原型
我们要的效果:
- 拦截器不能直接覆盖Array原型,会污染全局的Array
- 只针对那些被监听的数组进行拦截。
大致思路:
在Object 的变化侦测 中,我们实现了一个属性遍历侦测器Observer。是用来递归监听某个对象中所有属性的的一个类。我们就可在它递归监听时,判断该属性是否是数组,如果是数组,则在该子对象的原型中添加我们的副本原型(也就是我们的监听器)。
class Observer {
constructor(value) {
this.value = value
if (Array.isArray(value)) {
// 修改数组原型
value.__proto__ = arrayMethods
} else {
this.walk(value);
}
}
walk(obj) {
for (let key in obj) {
defineReactive(obj, key, obj[key]);
console.log('value', obj[key]);
console.log('key', key);
}
}
}
**注意:**如果浏览器不支持_proto_,就手动遍历副本原型的属性
// 判断浏览器是否能使用__proto__
const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
class Observer {
constructor(value) {
this.value = value
if (Array.isArray(value)) {
// 根据是否支持proto来决定使用哪种方式覆盖原型对象。
const augment = hasProto ? protoAugment : copyAugment
} else {
this.walk(value);
}
}
walk(obj) {
for (let key in obj) {
defineReactive(obj, key, obj[key]);
console.log('value', obj[key]);
console.log('key', key);
}
}
// 三个参数:target:要修改的对象;src:用于替换的对象,keys:替换对象的字段名
function protoAument (target, src, keys) {
target.__proto__ = src
}
function copyAugment(target, src, keys) {
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// 修改原型对象
def(target, key, src[key])
}
}
}
// 修改原型对象的工具方法
def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
注意:这里遍历对象属性为什么用 Object.getOwnPropertyNames 而不是Object.keys或者for in呢:
因为Array.prototype 中的属性都是不可枚举的属性。使用 Object.keys 和 for in 无法遍历到它们
6.收集依赖
在Observer类中存储依赖。
收集依赖时。每个值上新增一个_ob_字段,用于存储该值的Observer实例。判断某个元素已经创建Observer监听实例,则不用再次创建。