Vue 重写了数组原型上的方法,其他数组为何不被污染?

2,623 阅读3分钟

大家好,已经有好久没写文章了,整理了上次面试的一个面试题。希望对大家有帮助。

Vue 为了监听数组变化,重写了数组原型上的方法,那么重写后怎么保证其他数组不会被污染?

面试经历

回想上次面试经历记忆犹新:

面试官 :Vue 怎么实现数据双向绑定的?

(张口就背) :vue 使用了 Object.defineProperty 劫持了 get set 方法,通过...,

面试官 :好,defineProperty 不能代理数组你知道吧?那你知道 Vue 是怎么实现数组的响应式?

(心里窃喜,面试前有简单看过这里源码):Vue 重写了数组原型上的方法, 在重写的方法中触发了更新

面试官 :重写原型方法?那不是会使所有数组都受到影响? Vue 是怎么解决的?

我: .....

u=587394654,2139431731&fm=26&fmt=auto&gp=0.jpeg

Vue 源码解析

1、首先我们要知道创建了一个继承原型对象,这样无论怎么重写继承对象属性,都不会影响原对象。

// 数组原型对象
var arrayProto = Array.prototype;
// 继承数组原型对象
var arrayMethods = Object.create(arrayProto);

// 被重写的数组方法名
var methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
];

methodsToPatch.forEach(function (method) {
    
    // 缓存原始方法
    var original = arrayProto[method];
    
    // 覆盖新方法到继承对象上
    // arrayMethods - 继承对象
    def(arrayMethods, method, function mutator () {
        var args = [], len = arguments.length;
        while ( len-- ) args[ len ] = arguments[ len ];
        
		// 执行原方法
        var result = original.apply(this, args);
        var ob = this.__ob__;
        var inserted;
        debugger
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args;
                break
            case 'splice':
                inserted = args.slice(2);
                break
        }
        
        // push、unshift、splice 会产生新数组元素
        // 这里主要是将新元素 item 也加入到观察数组中
        if (inserted) { ob.observeArray(inserted); }
        // notify change
        ob.dep.notify();
        return result
    });
});

2、上面的操作已经将重写的方法挂载到继承原型对象 arrayMethods 了,接下来就是需要 proxy 的数组 __proto__ 指向 arrayMethods 就可以了, 例如:

const vueArray = ['a', 'b', 'c']
vueArray.__proto__ = arrayMethods

// 这里的 push 方法事实上是用的 arrayMethods 的push, 而不是 Array.protype.push
vueArray.push('d')

3、我们看看 Vue 是何时替换 __proto__

var Observer = function Observer (value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
        if (hasProto) {
            
            // 就是这个地方
            protoAugment(value, arrayMethods);
        } else {
            copyAugment(value, arrayMethods, arrayKeys);
        }
        this.observeArray(value);
    } else {
        this.walk(value);
    }
};

function protoAugment (target, src) {
    /* eslint-disable no-proto */
    target.__proto__ = src;
    /* eslint-enable no-proto */
}

在执行 new Observer(value) 时,value 如果是数组,那么 Vue 就会替换 value.proto = arrayMethods, 实现重写。

到此为止,就完成了重写数组原型方法。

步骤总结

  1. 创建一个继承数组原型的新对象
  2. 重写新对象中部分方法
  3. 将新的原型对象赋值给响应式数组上

回答上面的提问

“重写原型方法?那不是会使所有数组都受到影响? Vue 是怎么解决的?”

答: vue 重写的是自己继承的原型对象,而且只作用于响应式数据,不会污染原始数组原型。

引发思考

这里分享一下自己阅读源码的技巧:(大佬可略过)

带着问题看源码相比直接上手通篇阅读源码更有目标性,也能找到那种为了搞明白一个问题不断探索学习的精神。经过一些连续的提问然后去源码中找到答案,就像寻宝一样,寻宝的过程中 vue 源码也就熟悉的差不多了。

再例如之前一直不太理解 vue 中 computed 是怎么实现函数里面的变量响应式更新的,还能缓存,带着这个有趣的问题就又把 vue 源码又熟悉了一边,这里就不过多讲述具体过程,有机会后面专门出一篇文章。

在使用 vue 框架时,我们不仅需要深入掌握 vue 官网给出的 api, 也要大概知道其原理,那么,什么时候最适合去了解某个 api 的相关源码呢?其实就是抓住我们在使用 vue 过程中遇到的每一个小问题,遇到问题不仅仅是百度搜索出解决方案,并且需要带着问题去源码中找到属于我们自己的答案。