通过上述方法,我们实现了对数组原生方法进行功能的拓展,但是,有一个巨大的问题摆在面前:我们该如何让数组实例调用功能拓展后数组方法呢?
解决这一问题的方法就是:数组劫持。
数组劫持
数组劫持,顾名思义就是将原本数组实例要继承的方法替换成我们功能拓展后的方法。
想一想,我们在前面实现了一个功能拓展后的数组 arrayMethods ,这个自定义的数组继承自数组对象,我们只需要将其和普通数组实例连接起来,让普通数组继承于它即可。
而想实现上述操作,就是通过原型链。
实现方法如下代码所示:
let arr = [] // 通过隐式原型继承arrayMethods arr.__proto__ = arrayMethods // 执行变异后方法 arr.push(1)
通过功能拓展和数组劫持,我们终于实现了变异数组,接下来让我们看看 Vue 源码是如何实现变异数组的。
源码解析
我们来到 src/core/observer/index.js 中在 Observer 类中的 constructor 函数:
constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) // 检测是否是数组 if (Array.isArray(value)) { // 能力检测 const augment = hasProto ? protoAugment : copyAugment // 通过能力检测的结果选择不同方式进行数组劫持 augment(value, arrayMethods, arrayKeys) // 对数组的响应式处理 this.observeArray(value) } else { this.walk(value) } }
Observer 这个类是 Vue 响应式系统的核心组成部分,在初始化阶段最主要的功能是将目标对象进行响应式化。在这里,我们主要关注其对数组的处理。
其对数组的处理主要是以下代码
// 能力检测 const augment = hasProto ? protoAugment : copyAugment // 通过能力检测的结果选择不同方式进行数组劫持 augment(value, arrayMethods, arrayKeys) // 对数组的响应式处理,很本文关系不大,略过 this.observeArray(value)
首先定义了 augment 常量,这个常量的值由 hasProto 决定。
我们来看看 hasProto:
export const hasProto = '__proto__' in {}
可以发现, hasProto 其实就是一个布尔值常量,用来表示浏览器是否支持直接使用 __proto__ (隐式原型) 。
所以,第一段代码很好理解:根据根据能力检测结果选择不同的数组劫持方法,如果浏览器支持隐式原型,则调用 protoAugment 函数作为数组劫持的方法,反之则使用 copyAugment 。
不同的数组劫持方法
现在我们来看看 protoAugment 以及 copyAugment 。
function protoAugment (target, src: Object, keys: any) { /* eslint-disable no-proto */ target.__proto__ = src /* eslint-enable no-proto */ }
可以看到, protoAugment 函数极其简洁,和在数组变异思路中所说的方法一致:将数组实例直接通过隐式原型与变异数组连接起来,通过这种方式继承变异数组中的方法。
接下来我们再看看 copyAugment :
function copyAugment (target: Object, src: Object, keys: Array<string>) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] // Object.defineProperty的封装 def(target, key, src[key]) } }
由于在这种情况下,浏览器不支持直接使用隐式原型,所以数组劫持方法要麻烦很多。我们知道该函数接收的第一个参数是数组实例,第二个参数是变异数组,那么第三个参数是什么?
// 获取变异数组中所有自身属性的属性名 const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
arrayKeys 在该文件的开头就定义了,即变异数组中的所有自身属性的属性名,是一个数组。
回头再看 copyAugment 函数就很清晰了,将所有变异数组中的方法,直接定义在数组实例本身,相当于变相的实现了数组的劫持。
实现了数组劫持后,我们再来看看 Vue 中是怎样实现数组的功能拓展的。
功能拓展
数组功能拓展的代码位于 src/core/observer/array.js ,代码如下:
import { def } from '../util/index' // 缓存数组原型 const arrayProto = Array.prototype // 实现 arrayMethods.__proto__ === Array.prototype export const arrayMethods = Object.create(arrayProto) // 需要进行功能拓展的方法 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function (method) { // cache original method // 缓存原生数组方法 const original = arrayProto[method] // 在变异数组中定义功能拓展方法 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) // notify change ob.dep.notify() // 返回原生数组方法的执行结果 return result }) })
可以发现,源码在实现的方式上,和我在数组变异思路中采用的方法一致,只不过在其中添加了响应式的处理。
总结