Vue2 源码中数组劫持方法的实现

Vue2 中利用 Object.defineProperty 可以侦测对象属性变更的特性实现了数据响应,但它只是对象的方法,如果遇到了值为数组的话,是无法继续用 Object.defineProperty 来侦测的,如下:

new Vue({
    data: {
        a: 4, // 这个通过 Object.defineProperty 可以侦测到变化
        arr: [1, 2, 4], // 这个需要单独处理
    }
})

为了让数组的变化(插入,移除,排序)也能触发数据响应,Vue内部对数据单独做了处理,下面是基本的实现原理:

// 基于数组原型创建新的对象,这样这个对象就拥有了数组的方法,诸如 push,splice...
const myArrayObj = Object.create(Array.prototype);

// 这里以 push 方法作为示例,先缓存原始方法
const originPush = myArrayObj.push;

// 重写 myArrayObj 的 push 方法
Object.defineProperty(myArrayObj, 'push', {
    value(...args) {
        // 这样在用户调用数据的 push 方法时,就能拦截,比如更新依赖
        console.log('正在调用插入', arg);
        const result = originPush.apply(this, args);
        return result;
    }
});

// 有了这个对象以后,还要让具体的数据对象,比如 [1,2,3],拥有 myArrayObj 的能力
const arr = [1, 2, 3];

// 通过 __proto__ 可以改变一个对象的原型引用,这样 arr 就是从 myArrayObj 上找 push 方法了
arr.__proto__ = myArrayObj;


// 实际调用
arr.push(4); // 1,2,3,4

补充:有些浏览器对象上可能没有 __proto__ 属性,那 Vue 也做了处理,就是将 myArrayObj 上的方法,直接挂载到实例对象上:

// 定义对象的 push 属性
Object.defineProperty(arr, 'push', {
    value: myArrayObj.push
});

// 或者简单理解为:
arr.push = myArrayObj.push;