数组变异方法的实现原理

path: vue/src/core/observer/array.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import { def } from '../util/index'

const arrayProto = Array.prototype
// 原型式继承数组原型对象
/*
* Object.create(arrayProto)的操作等同于下面的操作
*
* arrayMethods = {};
* Object.setPrototypeOf(arrayMethods, arrayProto);
*
* */

export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]

/**
* Intercept mutating methods and emit events
*
* 给 arrayMethods 变异方法
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
// def,使用defineProperty是定对象属性的value
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)
}
// 既然对象成员个数新增,就需要通知订阅者,当前它依赖的数据已经发生变动
ob.dep.notify()
return result
})
})
1
2
3
4
5
6
7
8
9
10
11
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}

整段代码执行下来,创建了一个对象arrayMethods,它的原型只想数组原型,并且它有这么成员方法:’push’,
‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’!

很明显,这样还不够!这样还能直接通过vue数组实例的点操作符调用变异方法!还需要将这些挂在vue实例数组的原型链上!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export class Observer {
// ...

constructor (value: any) {
// ...
if (Array.isArray(value)) {
if (hasProto) {
// 通过__proto__将数组的原型指向arrayMethods
protoAugment(value, arrayMethods)
} else {
// 如果没有__proto__,说明不能通过__proto__设置原型指向!
// 则直接将变异的数组方法作为OwnProperty直接挂载在数组上
copyAugment(value, arrayMethods, arrayKeys)
}
// ...
}
// ...
}
// ...
}

在坚持数组时,会将arrayMethods,根据实际情况挂在到当前这个数组的原型链上!在可以设置原型执行时,直接改变当前数组原型指向,改为arrayMethods;否则,直接将arrayMethods的方法,复制到当前数组上,作为当前数组的成员方法!

回看上面的代码(这一处:def(arrayMethods, method, function mutator (...args)),在这一块代码可以发现this.__ob__!经过以上分析,我们知道arrayMethods最后会挂在到vue数组上,那么这个this指向的就是这个数组,那么__ob__应该就是在vue数组原型链上或数组ownProperties上的!

似曾相似,在哪里遇过~

1
2
3
4
5
6
7
8
9
10
export class Observer {
// ...

constructor (value: any) {
// ...
def(value, '__ob__', this)
// ...
}
// ...
}

在创建Observer实例时,会将当前实例挂在到当前观察数据的__ob__上,对vue数组而言,这个被观察的数据就是它了!

既然__ob__是Observer实例,当然调用劫持数组的方法ob.observeArray(inserted)!这里插一句题外话,虽然新增元素是属于当前数组的,但还是在被劫持这个行为上,他们是相互独立的,所以这里就是不使用ob.ob.observeArray去劫持,而使用inserted.forEach((item) => observe(it))劫持也是可以的~

这里需要当前数组的__ob__,主要是为了通知到这个数组的订阅者!

总结

  • 变异方法通过对数组原型方法的拦截对原有方法进行处理,拦截的方式分两种:1. 可以设置原型的情况下,通过改变vue数组的原型指向进行拦截;2. 1不可行的情况下,则在vue数组的ownProperties上创建同名成员方法拦截!
  • 编译数组方法可以通知watche,是因为最后调用ob.dep.notify(),通知了订阅者,这就是本质区别;
  • ob.observeArray(inserted)再次坚持新元素是为了让别的watcher可以订阅新属性。