在VUE源码解析之变化侦测(一)中知道VUE中Object的变化是靠setter来追踪,只要一个数据发生变化,就会触发setter,进而遍历告知Dep里面的Watcher,Watcher再进一步进行相应的处理。接下来探讨下VUE中的Array侦测。
VUE中Array的数据侦测
array和object大有不同,如下例子:
this.list.push("binguo")
当我们时候list方法的时候,根本没有触发到getter/setter方法,所以我们得通过第二方法侦测。你可能会想到了,可不可以看它有没有触发push这个方法?但可惜ES6之前,JS并没有提供元编程的能力,也就是没有提供可以拦截原生的原型方法。但难不倒尢雨溪大大,我们可以通过拦截器来实现。
拦截器
其实理解起来也很简单,就是在Array使用push方法时,将其拦截进行相应操作。我们可以实现一个这样的拦截器:与Array.prototype有一样的Object,里面包含的属性也一模一样,只不过这个object中某些改变数组自身的方法(push/pop/shift/unshift/splice/sort/reverse)被我们处理过!
const arrayProto = Array.prototype;
export 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
})
})
上述代码可以看出,我们创建了一个arrayMethods方法,它继承了所有Array.prototype的属性,接下来,使用Object.defineProperty对改变数组自身的方法进行封装。这样,当我们调用arrayMethods.push时,其实调用的是mutator方法,在方法的最后才执行original(其是原生Array.prototype上的方法)原来的方法,比如push功能。
想要让拦截器生效,就要去覆盖原来的Array.prototype。但又不能直接覆盖,因为这样做会污染全局变量。所以我们只需要覆盖那些需要侦测的数组就可以了,也就是Observer中的属性。
export class Obsrver{
constructor(value) {
this.value = value
if (Array.isArray(value)) {
value.__proto__=arrayMethods //将其原型指向arrayMethods
} else {
this.walk(value)
}
}
}
从上面代码看理解起来其实很简单的,当我们遍历侦测属性时,如果其是Array类型,就将其的原型指向arrayMethods即可。如果浏览器不支持__proto__的话,我们需要将arrayMethods身上的这些方法设置到背侦测的数组上,这样,当数组访问的时候就直接访问数据上的方法,不会继续访问原型的方法。如下代码:
import {arrayMethods} from "./array";
const hasProto = '__proto__' in {}//判断__proto__是否可用
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export class Obsrver {
constructor(value) {
this.value = value
if (Array.isArray(value)) {
value.__proto__ = arrayMethods //将其原型指向arrayMethods
constructor(value)
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment
augment(value,arrayMethods,arrayKeys)
}
} else {
this.walk(value)
}
}
......
}
function protoAugment(target, src, keys) {
target.__proto__ = src
}
function copyAugment(target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++){
const key = key[i]
def(target,key,src[key]) //该函数作用是:在target上添加key属性,value值为src[key]。
}
}
我们现在已经知道如何进行数组的侦听,接下来,就是要做的是当数组变化时通知谁,也就是收集依赖!
收集依赖
其实收集依赖也很容易理解,因为每次访问数组的时候,首先都会访问其数组名。因为数组名也是一个key,所以,我们只需要和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: function () {
dep.depend() //收集Array的依赖
return val
},
set: function (newVal) {
if (val === newVal) {
return
}
dep.notify() //触发依赖
val = newVal
}
})
}
但Vue中Array的Dep缺不是放在Observer中,而是放在Observer中,
export class Observer {
constructor(value) {
this.value = value
this.dep = new Dep() //新增dep
def(value,'__ob__',this) //value上添加属性__ob__
if (Array.isArray(value)) {
value.__proto__ = arrayMethods //将其原型指向arrayMethods
constructor(value)
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment
augment(value,arrayMethods,arrayKeys)
}
} else {
this.walk(value)
}
}
}
这里可能会问为什么要放在Observer中?我们要知道依赖保存的位置很关键,它必须在getter和拦截器中可以访问到。
在上面可以看到下面这段代码:
def(value,'__ob__',this) //value上添加属性__ob__
其中value则是传进来的Array,在其身上添加一个__ob__的属性,值为当前的Observer。因为Array拦截器是对原型的一种封装,所以在拦截器中可以访问到this(当前正被操作的数组)。所以也可以借此通过this.__ob__来访问Observer进而可以拿到里面的Dep。这里我们在相应的defineReactice修改一下!
function defineReactive(data, key, val) {
let childOb = observe(val) //修改
let dep = new dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend()
if (childOb) { //是否Observer实例
childOb.dep.depend();
}
return val
},
set: function (newVal) {
......
}
})
}
//尝试为value创建一个Obserrver实例,
//如果创建成功,直接返回新创建的Observer实例。
//如果已经存在一个Observer实例,则返回它
export function observe(value, asRootDate) {
if (!isObject(value)) {
return
}
let ob
if (hasOwn(value, '__ob__' && value.__ob__ instanceof Observer)) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
上面的可以看到,如果value是相应数据,即含有__ob__属性就不需要再创建Observer实例,直接返回已创建的Observer即可,避免重复侦测value变化的问题。这样,当value身上标记了__ob__之后,拦截器也顺理成章可以访问到Observer的实例了。
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
//缓存原始的方法
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args) {
const ob = this.__ob__.Dep//获取Observer中的收集器
ob.dep.nitify() //向依赖发送信息
return original.apply(this, args)
},
enumerable: false,
writable: true,
configurable: true
})
})
这已经完了?还没有,这仅仅是给第一次数组添加了侦听。
子数据侦测
无论是object还是array我们都需要对所有响应数据的子数据进行侦测。所以我们还需要遍历一下:
export class Observer {
constructor(value) {
this.value = value
this.dep = new Dep() //新增dep
def(value, '__ob__', this) //value上添加属性__ob__
if (Array.isArray(value)) {
this.observeArray(value)
value.__proto__ = arrayMethods //将其原型指向arrayMethods
constructor(value)
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
}
} else {
this.walk(value)
}
}
observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
这仅仅是遍历了侦测原有的数据,如果是通过push网数据新增元素,这个新增的元素也是要侦测的,其实思路挺简单,我们只需要知道哪些是新增的属性,再对其进行侦测就好。数组中新增的数据的方法有push、unshift和splice,我们只需要进行判断,再把新增的元素取出来,如下:
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args) {
const ob = this.__ob__.Dep
let addData
switch(method){ //判断方法类型
case 'push':
case 'unshift':
addData = args
break
case 'splice':
addData = args.slice(2)
break
}
if(addData) this.ob.observeArray(inserted) //新增侦测
ob.dep.nitify()
return original.apply(this, args)
},
enumerable: false,
writable: true,
configurable: true
})
})
如上代码,我们只需要获取到添加的元素,然后通过调用Observer中的observerArray方法添加侦测即可。
Vue中Array侦测存在的一些问题
当我们修改的时候,我们只能通过调用其拦截器中的方法才会被侦测到,如果我们像下面那样
this.list[2]="binguo"
我们会发现是侦测不到的,需要this.list数据更新了,但视图仍然是旧数据,无法侦测到数据的变化,又例如
this.list.length = 0
这个清空数组操作也是如此,需要手动刷新或者进行一次重新赋值才会发送变化,这是ES5无法实现的,但在ES6利用proxy可以实现这些侦测,所以在Vue2.0我们需使用官网提供的vm.$set对数组进行这些操作。
注:本文主要参考自《深入浅出Vue.js》