上一篇介绍了关于Object的变化侦测,这篇介绍关于Array.
我们在修改Array时,使用的是Array 的原型上的方法(Push,Pop,Shift等),而这些操作不会触发getter/setter.所以对于Object的侦测方法对Array行不通.而ES6之前,js并未提供元编程能力,故我们无法拦截原型方法,但我们可以用自定义方法去覆盖原型方法.
因此我们可以用一个拦截器去覆盖Array.prototype.拦截器原理如图:
1.Array拦截器:首先自定义arrayMethods,让其原型指向Array.prototype,并修改其对应methods中的方法.当调用arrayMethods的同名方法时,我们实际执行的函数时mutator方法,而其最后调用Array.prototype的原生方法,而在此之前,我们可以加入自己的逻辑,比如说发送变化通知等等.
// 会改变数组的方法
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayProto = Array.prototype // 复制一个Array.prototype对象
const arrayMethods = Object.create(arrayProto) // 这个是我们自己定义的原型对象
// 循环arr,重写arr中的相关方法
methods.forEach((method) => {
// 先保存原始方法
const original = arrayProto[method]
// 在新的原型对象中重写自己的方法
Object.defineProperty(arrayMethods, method, {
value: function mutator (...args) {
// 在执行原始方法前可以加入我们自己的逻辑
// 返回原始方法的调用
return original.apply(this, args)
},
enumerable: false,
writable: true,
configurable: true
})
})
有了拦截器,我们不能直接覆盖Array.prototype,因为这样会污染全局的Array,也就是说我们只希望这个拦截器去覆盖那些响应式数组的原型.而我们是通过Observer类来将数据转换为响应式数据(详见Object变化侦测),所以我们只需要在Observer扫描到数组时用拦截器覆盖其原型即可.
export default class Observer {
// 参数就是需要侦测的数据。首次进来参数肯定是vue的data对象无疑
constructor (data) {
this.value = data
if (Array.isArray(this.value)) {
// 我们这次新增的代码,改变原型指向
this.value.__proto__ = arrayMethods
} else {
// 如果不是数组,必然就是对象
this.walk(this.value) // vue内部用来递归劫持数据的方法
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
但是现在问题是,.__proto__并不是所有浏览器都支持的,如果不支持的情况下怎么办。很简单,如果不知道,我们就直接把相关方法写入到该数组中去。根据原型链原理,数组调用某个方法时,优先调用自己的方法,如果不存在此方法,才会去原型对象中找。
// 判断是否支持.__proto__属性
const hasProto = '__proto__' in {}
// 获取原型对象下所有方法名的集合
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
/**
* @description 定义数组自己的方法
* @Params obj: 需要劫持的数组对象,key:需要设置的属性名,如push。 val: obj.push的值,是一个function
* @Author chen
*/
function def (obj, key, val) {
Object.defineProperty(obj, key, {
value: val,
enumerable: false,
writable: true,
configurable: true
})
}
/**
* @description 浏览器支持__proto__属性时的执行函数
* @Params target: 需要劫持的数组, src: 复制的原型对象arrayMethods keys: arrayMethods中的方法名集合
* @Author chen
*/
function protoAugment (target, src, keys) {
target.__proto__ = src
}
/**
* @description 浏览器不支持__proto__属性时的执行函数
* @Params target: 需要劫持的数组, src: 复制的原型对象arrayMethods keys: arrayMethods中的方法名集合
* @Author chen
*/
function copyAugment (target, src, keys) {
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
// 将需要被劫持的数组的.__proto__属性指向我们自己定义的原型对象arrayMethods
export default class Observer {
// 参数就是需要被劫持(侦测)的数据。首次进来参数肯定是vue的data对象无疑
constructor (data) {
this.value = data
if (Array.isArray(this.value)) {
// 更改的地方
const runFun = hasProto ? protoAugment : copyAugment
// 执行具体函数,目的反正都是重写数组方法
runFun(value, arrayMethods, arrayKeys)
} else {
// 如果不是数组,必然就是对象
this.walk(this.value) // vue内部用来递归劫持数据的方法
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
2.收集依赖 :
数组收集依赖的方法与Objcet一样,都是在getter中收集的.所以,Array在getter中收集依赖,在拦截器中触发依赖.
但依赖应该存储在哪呢?vue把数组的Dep存储在Observer实例中.因为这样可以保证无论是getter还是拦截器都可以访问到这个Dep.
// 将需要被侦测的数组的.__proto__属性指向我们自己定义的原型对象arrayMethods
export default class Observer {
// 参数就是需要侦测的数据。首次进来参数肯定是vue的data对象无疑
constructor (data) {
this.value = data
// 新增dep
this.dep = new Dep()
// 此函数得作用是在data(需要劫持得数组)下新增一个__ob__属性, 属性值是this(当前this就是new Observer出来得oberver实例)。同时在data下新增一个这样得属性,还可以作为当前数据是否已经是响应式数据得一个标识,如果是响应式数据,那么肯定就会有这个属性
def(data, '__ob__', this)
if (Array.isArray(this.value)) {
const runFun = hasProto ? protoAugment : copyAugment
// 执行具体函数,目的反正都是重写数组方法
runFun(value, arrayMethods, arrayKeys)
} else {
// 如果不是数组,必然就是对象
this.walk(this.value) // vue内部用来递归劫持数据的方法
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
/**
* @description 用于返回当前observer实例,如果当前val存在__ob__属性,说明是响应式数 据,直接返回val的__ob__的值
* 否则说明是非响应式数据,也就是还没有被劫持过。那么直接返回new Observer()
* @Author chen
*/
function observe (val) {
let ob = ''
if (hasOwn(value, '__ob__') && value.__ob__ instanceof of Observer) {
ob = value.__ob__
} else {
ob = new Observer()
}
return ob
}
function defineReactive(data, key, val) {
//如果是数组返回数组的observer实例,是对象则递归侦测
if (val.toString() === '[Object Array]') {
let childObserver = observe(val)
} else if (val.toString() === '[Object Object]') {
new Observer(val)
}
let dep = new Dep()
Object.defineProperty(data, key, function() {
enumerable: true, // 该属性是否可被枚举
configurable: true, // 该属性的配置后续是否可进行更改
get: function () { // 用于获取属性值
// 收集依赖
if (window.target) {
dep.addSub(window.target)
// 新增数组的依赖
if (childObserver) {
childObserver.dep.addSub(window.target)
}
}
return val
},
set: function (newVal) { // 设置属性值
if (newVal === val) return
// 用新值替换旧值
val = newVal
// 数据发生变化时,通知依赖者
dep.notify()
}
})
}
此时,如此一来,我们可以通过被侦测数组的__ob__属性来获取对应于它的Observer实例,再通过observer.dep来获取对应于它的Dep.对该dep进行添加或循环通知信息.
修改后的拦截器方法如下:
// 循环arr,重写arr中的相关方法
methods.forEach((method) => {
// 先缓存原始方法
const original = arrayProto[method]
// 在新的原型对象中重写自己的方法
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args) {
//发送更新通知,这里的this指代调用方法的array
this.__ob__.dep.notify()
// 返回原始方法的调用结果
return original.apply(this, args)
},
enumerable: false,
writable: true,
configurable: true
})
})
3.侦测数组中元素变化 :无论是Object还是Array,响应式数据的子数据都要被侦测.
function observeArray (items) {
for (let item of items) {
observe(item)
}
}
//然后在Observer构造器关于数组的逻辑中编写
if (Array.isArray(this.value)) {
// 对每一项都执行new Observer()
this.observeArray(this.value)
// 更改的地方
const runFun = hasProto ? protoAugment : copyAugment
// 执行具体函数,目的反正都是重写数组方法
runFun(value, arrayMethods, arrayKeys)
} else {
// 如果不是数组,必然就是对象
this.walk(this.value) // vue内部用来递归劫持数据的方法
}
而对于数组来说,有三个新增操作push,unshift,splice,我们只需要在拦截器中对这三种情况进行单独处理即可:
Object.defineProperty(arrayMethods, method, {
value: function myMethod (...args) {
let inserted // 用来存放新增的元素
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
default:
break
}
if (inserted) {
// this.__ob__获取到当前observer实例,然后用这个实例调用observerArray方法,对这个对象以及他的子级进行数据侦测
this.__ob__.observeArray(inserted)
}
// 这里可以发送更新通知
this.__ob__.dep.notify()
// 返回原始方法的调用结果
return original.apply(this, args)
},
enumerable: false,
writable: true,
configurable: true
})
})
PS 关于Array的一些问题
对Array的数据侦测是通过拦截原型方法实现的,而正是因为这种方式,我们无法侦测到一些数据操作所造成的数组变化,如 this.arr[0] = 2,this.arr.length = 0 等.
本文章仅为本人学习总结,如果有不足还请各位指出!