小言_互联网的博客

3 / 30 从源码上探究 Vue 是如何对数组方法进行变异的

258人阅读  评论(0)

前面的话

前端日问,巩固基础,不打烊!!!

解答

首先:Object.defineProperty是可以对数组进行劫持的,但对后来新添加的属性是不会劫持的。
也就是说,Object.defineProperty是根据数组的索引来监听数组的变化的,只不过不能劫持后添加的属性。

来看例子:

 function defineReactive(data,key,value){
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                console.log(`get key: ${key} value: ${value}`);
                return value;
            },
            set: function(newValue){
                console.log(`set key: ${key} value: ${newValue}`);
                value = newValue;
            }
        })
    }

    function observe (data) {
        Object.keys(data).forEach(function(key){
            defineReactive(data,key,data[key]);
        })
    }

    let arr = [1, 2, 3];
    observe(arr);


这说明Object.defineProperty是可以通过数组的索引来监听数组的变化 。

我们接着看,给数组添加一个值会怎样:


所添加的值,并没有触发上面的settergetter方法,原因就是并没有对新添加的下标进行observer

总结一下:

Object.defineProperty对数组的劫持与对象一样,会把数组的索引当作key来监听数组,只是只能监听初始的索引变化。如果使用pushshift来增加索引,需要再手动初始化才能被observer

Observer源码如何实现

在源码上,Observer对数组进行了单独的处理

  • 首先来看看hadProto 这个常量,定义如下:

    export const hasProto = '__proto__' in {}
    

    简单来说,就是用来表示浏览器是否支持直接使用__proto__,一个布尔值。

    即如果浏览器支持隐式的原型__proro__,则调用protoAugment方法,否则调用copyAugment方法。

  • 下面看一下protoAugment方法

    function protoAugment (target, src: Object, keys: any) {
      /* eslint-disable no-proto */
      target.__proto__ = src
    }
    

    直接将数组的实例通过__proto__arrayMethods对象连接起来。从而继承了arrayMethods上的方法。

  • 接着看arrayMethods是如何定义的

    // 缓存数组原型
    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) {
      // 缓存原生数组方法
      const original = arrayProto[method];
      def(arrayMethods, method, function mutator(...args) {
        // 执行并缓存原生数组功能
        const result = original.apply(this, args);
        // 响应式处理
        const ob = this.__ob__;
        let inserted;
        switch (method) {
        // push、unshift会新增索引,所以要手动observer
          case "push":
          case "unshift":
            inserted = args;
            break;
          // splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。
          case "splice":
            inserted = args.slice(2);
            break;
        }
        // 
        if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听
        // notify change
        ob.dep.notify();// 通知依赖更新
        // 返回原生数组方法的执行结果
        return result;
      });
    });
    
    };
    

    arrayMethods 是 原生数组的一个实例,然后遍历methodsToPatch数组,对每一个元素进行def

    def方法的作用,如果不深究的话,可以粗略的表示为:

    arrayMethods[method] = function mutator(){};
    

    如果是:push、unshfit、splice方法添加了新的元素,就要将添加的元素Observer一次,使其变为响应式。

下面附上defobserverArray方法的源码:

  • def 方法源码:

    methodsToPatch中的每一个方法(push、shfit等)进行数据劫持,并且挂载在arrayMethods对象上。

    export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
      Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
      })
    }
    
  • observerArray方法源码

    对数组的每一个元素进行监听,使其变为响应式。

    Observer.prototype.observeArray = function observeArray(items) {
      for (var i = 0, l = items.length; i < l; i++) {
        observe(items[i]);
      }
    
小结:

总的来说,就是重写了原生的方法,通过target__proto__ == arrayMethods来改变了数组实例的原型。arrayMethods上的每一个属性(即push、shfit)都被重写了。

注意:改变了原型链,target__proto__==arrayMethods;arrayMethods.proto==Array.Prototype,通过改变原型链来进行数组方法的重写。

Object.defineProperty VS Proxy

  • Object.defineProperty只能劫持对象的属性(需要遍历),而Proxy是直接代理对象。

  • Object.defineProperty对新增属性需要手动进行Observe。
    也正是这个原因,Vue给data中的数组或者对象新增属性时,需要使用vm.$set才能保证新增属性也是响应式的。

    附上set的源码:

    
    /**
     * Set a property on an object. Adds the new property and
     * triggers change notification if the property doesn't
     * already exist.
     */
    export function set (target: Array<any> | Object, key: any, val: any): any {
      // 如果target是数组,且key是有效的数组索引,会调用数组的splice方法,
      // 我们上面说过,数组的splice方法会被重写,重写的方法中会手动Observe
      // 所以vue的set方法,对于数组,就是直接调用重写splice方法
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        target.splice(key, 1, val)
        return val
      }
      // 对于对象,如果key本来就是对象中的属性,直接修改值就可以触发更新
      if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
      }
      // vue的响应式对象中都会添加了__ob__属性,所以可以根据是否有__ob__属性判断是否为响应式对象
      const ob = (target: any).__ob__
      // 如果不是响应式对象,直接赋值
      if (!ob) {
        target[key] = val
        return val
      }
      // 调用defineReactive给数据添加了 getter 和 setter,
      // 所以vue的set方法,对于响应式的对象,就会调用defineReactive重新定义响应式对象,defineReactive 函数
      defineReactive(ob.value, key, val)
      ob.dep.notify()
      return val
    }
    

    在 set 方法中,对 target 是数组和对象做了分别的处理,target 是数组时,会调用重写过的 splice 方法进行手动 Observe 。

    对于对象,如果 key 本来就是对象的属性,则直接修改值触发更新;否则调用 defineReactive 方法重新定义响应式对象。

  • Proxy 是直接可以通过set(target, propKey, value, receiver)拦截对象属性设置,是可以拦截到对象的新增属性的。

  • Proxy支持13种拦截操作,这是defineProperty所不具有的

参考文章:


转载:https://blog.csdn.net/qq_41257129/article/details/105196145
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场