【深入浅出Vue.js篇】之Array的变化侦测
前言
在上一篇文章中我们使用Object.defineProperty实现了Object的变化侦测,但是Array在触发push方法的时候就不能通过getter/setter来实现侦测了,那么Array的变化侦测要怎么实现呢,看完这篇文章你就会知道啦。
提示:以下是本篇文章正文内容,下面案例可供参考
一、如何追踪变化?
Object的变化是靠setter来追踪的,只要数据发生了变化就可以触发setter方法。
那么Array的值变化的时候要怎么追踪呢?我们在用户调用操作数组方法(push,pop…)的时候做点什么,比如拦截一下是不是就可以实现数据侦测了呢?接下来我们就浅试一下。
二、拦截器
Array 原型中可以改变数组自身内容的方法有7个,分别是push 、pop 、shift 、unshift 、splice 、sort 和reverse 。
代码如下(示例):
// 拦截器
const arrayProto = Array.prototype
// 创建 arrayMethods 继承array 的原型
const arrayMethods = Object.create(arrayProto)
['push', 'shift', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach((method) => {
const original = arrayProto[method]
// Object.defineProperty(arrayMethods, method, {
// configurable: true,
// enumerable: false,
// writable: true,
// // 在操作数组时,调用的是mutator函数,里面可以做一些其他事,例如来发送变化通知等等。
// value: function metator (...args) {
// return original.apply(this, args)
// }
// })
//工具函数
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
// 侦测新元素变化
// 1.如果method 是push 、unshift 、splice 这种可以新增数组元素的方法
//那么从args 中将新增元素取出来,暂存在inserted 中。
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break;
case 'unshift':
inserted = args.slice(2)
break;
}
// 如果有新增元素,则使用ob.observeArray 来侦测这些新增元素的变化。
if(inserted)ob.observeArray(inserted)
ob.dep.notify() //向依赖(Watcher )发送消息
return result
})
})
在上面的代码中,我们创建了变量arrayMethods ,它继承自Array.prototype ,具备其所有功能。未来,我们要使用arrayMethods 去覆盖Array.prototype 。
接下来,在arrayMethods 上使用Object.defineProperty 方法将那些可以改变数组自身内容的方法(push 、pop 、shift 、unshift 、splice 、sort 和reverse )进行封装
所以,当使用push 方法的时候,其实调用的是arrayMethods.push ,而arrayMethods.push 是函数mutator ,也就是说,实际上执行的是mutator 函数。
最后,在mutator 中执行original (它是原生Array.prototype 上的方法,例如Array.prototype.push )来做它应该做的事,比如push 的功能。
三.使用拦截器覆盖Array原型
有了拦截器,想要让他生效,那就需要他覆盖掉Arraay.prototype,但这样显然是不可行的,因为直接覆盖会影响到全局的Array。
之前我们要将一个数据变成响应式数据,是通过Observer,同理 我们只需要在Observer中将Array的原型覆盖掉就好啦。
同时我们还要注意一下 不是所有的浏览器都支持 ‘__ proto __’,因此当浏览器不支持的时候我们不能直接覆盖,而是通过将拦截器挂载到vlaue上
代码如下(示例):
// __proto__是否可用
const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
class Observer{
constructor(value) {
this.value = value
this.dep = new Dep()
// 新增一个不可枚举的属性 __ob__
// 所有被侦测了变化的数据身上都会有一个 __ob__ 属性来表示它们是响应式的
def(value,'__ob__',this)
if (Array.isArray(value)) {
// 覆盖原型的功能
// 判断浏览器是否支持__proto__
//const augment = hasProto ? protoAugment : copyAugment;
//augment(value,arrayMethods,arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
}
/**
* 循环Array 中的每一项,执行observe 函数来侦测变化
* 将数组中的每个元素都执行一遍new Observer
*/
function observeArray (items) {
for (let i = 0; i < items.length; i++){
observe(items[i])
}
}
// 覆盖原型
function protoAugment (target, src, keys) {
target.__proto__ = src
}
// 将拦截器挂载到vlaue上
function copyAugment (target, src, keys) {
for (let i = 0; i < keys.length; i++){
const key = keys[i]
def(target,key,src[key])
}
}
工具函数
function def (obj,key,val,enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable:true
})
}
当value 身上被标记了 ob 之后,就可以通过value.ob 来访问Observer 实例。如果是Array 拦截器,因为拦截器是原型方法,所以可以直接通过this.ob 来访问Observer 实例
四.收集依赖
看过上一篇的人都知道,在实现Object数据侦测时,我们将依赖收集在Dep中,同理Array的依赖也同样是存放在Dep中的。
也就是说我们接下来要实现的就是在getter中收集依赖 并将依赖统一存放在Dep中进行管理。
function defineReactive (data, key, val) {
// Observer 实例
let childOb = observe(val)
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: TreeWalker,
get: function () {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
return val
},
set: function (newValue) {
if (val === newValue) {
return
}
// 通知
dep.notify()
val == newValue
}
})
}
/**
* 尝试为value创建一个Observer实例
* 如果创建成功,直接返回创建的Observe实例
* 如果value已经存在一个Observer实例,则直接返回它
*/
function observe (value, asRootData) {
if (!isObject(value)) {
return
}
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
总结
Array 追踪变化的方式和Object 不一样。因为它是通过方法来改变内容的,所以我们通过创建拦截器去覆盖数组原型的方式来追踪变化。
为了不污染全局Array.prototype ,我们在Observer 中只针对那些需要侦测变化的数组使用 __ proto __ 来覆盖原型方法,但 __ proto __ 在ES6之前并不是标准属性,不是所有浏览器都支持它。因此,针对不支持 __ proto __ 属性的浏览器,我们直接循环拦截器,把拦截器中的方法直接设置到数组身上来拦截Array.prototype 上的原生方法。
Array 收集依赖的方式和Object 一样,都是在getter中收集。但是由于使用依赖的位置不同,数组要在拦截器中向依赖发消息,所以依赖不能像Object 那样保存在defineReactive 中,而是把依赖保存在了Observer 实例上。
在Observer 中,我们对每个侦测了变化的数据都标上印记 ob ,并把this (Observer 实例)保存在 ob 上。这主要有两个作用,一方面是为了标记数据是否被侦测了变化(保证同一个数据只被侦测一次),另一方面可以很方便地通过数据取到 ob ,从而拿到Observer 实例上保存的依赖。当拦截到数组发生变化时,向依赖发送通知。
除了侦测数组自身的变化外,数组中元素发生的变化也要侦测。我们在Observer 中判断如果当前被侦测的数据是数组,则调用observeArray 方法将数组中的每一个元素都转换成响应式的并侦测变化。
除了侦测已有数据外,当用户使用push 等方法向数组中新增数据时,新增的数据也要进行变化侦测。我们使用当前操作数组的方法来进行判断,如果是push 、unshift 和splice 方法,则从参数中将新增数据提取出来,然后使用observeArray 对新增数据进行变化侦测。
由于在ES6之前,JavaScript并没有提供元编程的能力,所以对于数组类型的数据,一些语法无法追踪到变化,只能拦截原型上的方法,而无法拦截数组特有的语法,例如使用length 清空数组的操作就无法拦截。