Vue拦截数组变异方法的思路

 

Vue拦截数组变异方法的思路

在JavaScript的数组中,有一些可以改变原数组的方法,我们称之为数组的变异方法,包括splice、 sort、push、pop、reverse、shift、unshift等,当我们在vue中通过这些方法更改数组的时候会触发视图更新,这是为什么呢?

在上一篇文章中,我说明了通过Object.defineProperty可以实现对对象的监听,从而触发视图更新,JavaScript中数组也是对象的,为什么vue不使用Object.defineProperty来完成对数组的监听呢?通过网上查阅,发现使用Object.defineProperty监听数组性能很差,方便性得到的好处小于性能带来的损失,得不偿失。

那我们如何通过监听这些变异数组来触发视图更新呢?

试想下面的例子, 我如何不改变原函数的情况下让其先输出我就在这,等你来

function love() {
  console.log('你爱来不来')
}

思索中。。。

let tmpLove = love
function love() {
  console.log('我就在这,等你来')
  tmpLove()
}

咦,上面代码就完美实现了我的要求,首先将love函数缓存在tmpLove变量中,之后再重写love函数。这样我们就保证了在不变love函数的情况下,实现对其自身的扩展。

目前的Vue中,正是利用这种技巧实现对数组变异方法的扩展,即在不改变自身的前提下扩展方法。我们都知道,JavaScript中这些变异方法都是存在于数组构造函数的原型上,如图

数组也是对象,所以实例对象的__proto__ 属性指向的就是数组构造函数的原型,即array.__proto__ === Array.prototypetrue,为了拦截到数组的变异方法,我们可以在实例和原型之间在插入一层,如下图

中间层我们给他起个名字arrayMethods, 我们让数组实例array的__proto__属性指向arrayMethods, arrayMethods__proto__属性指向Array.prototype,这样,当我们通过实例去访问数组的变异方法时,根据原型链的查找规则,会先在arrayMethods对象中查找, 这样,我们就实现了对数组变异方法的拦截, 代码如下

// 数组的变异方法
const variationMethods = ['splice', 'sort', 'push', 'pop', 'reverse', 'shift', 'unshift']
// 缓存数组的原型对象
const tmpArrayPrototype = Array.prototype
// middLayer继承数组的原型对象,即middleLayer.__proto__ = Array.prototype
const middleLayer = Object.create(Array.prototype)
variationMethods.forEach(method => {
  // 变异方法全部在middleLayer对象上实现一遍
  middleLayer[method] = function(...args) {
    // 实际执行的还是Array.prototype对象上的方法
		const result = tmpArrayPrototype[method].apply(this, args)
    console.log(`拦截到了${method}方法的执行`)
    return result
  } 
})

我们来验证一下

const arr = []
arr.__proto__ = middleLayer
arr.push('1')

执行以上代码,会输出拦截到了push方法的执行, 同时arr数组的值变成了["1"],这说明我们拦截到了push方法,并且执行了push方法。

但是此时又会有一个问题,__proto__属性并不是所有的浏览器都支持啊,所以就得另想一个办法去拦截,这次我们直接在实例上添加方法, 代码如下

const arr = []
const arrayKeys = Object.getOwnPropertyNames(middleLayer)

arrayKeys.forEach(method => {
  arr[method] = middleLayer[method]
})

题外知识点

Object.keysObject.getOwnPropertyNames 都可以获取到属性名,但是它俩有什么区别呢?

Object.keys 用来获取对象自身可枚举的属性键

Object.getOwnPropertyNames 用来获取对象自身的全部属性名

我们继续

但是这样有一个问题,就是在实例上添加的方法是可以被枚举出来的,这和原来的方法表现不一致,所以我们可以通过Object.defineProperty来定义相关方法,如下

const arr = []
const arrayKeys = Object.getOwnPropertyNames(middleLayer)
arrayKeys.forEach(method => {
  Object.defineProperty(arr, method, {
    enumerable: false,
    writable: true,
    configurable: true,
    value: middleLayer[method]
  })
})

我们同样可以验证一下

arr.push('1')

结果和上面一致,这样我们就完成了对数组变异方法的拦截,至于拦截后如何处理,如何触发相关视图变化,这就需要阅读vue源码了。(ps: 后期可能会补上,^_^)