Vue2源码解析 Array的变化侦测

目录

1  如何追踪变化

2  拦截器 

3  使用拦截器覆盖Array原型

4  将拦截器方法挂载到数组的属性上

 5  如何收集依赖

6  依赖列表存在哪儿

7  收集依赖

8  在拦截器中获取Observer实例

9  向数组的依赖发送通知

10  侦测数组中元素的变化

11  侦测新增元素的变化

12  关于Array的问题

13  总结

1  如何追踪变化

es6之前,js没有提供元编程的能力,页没有提供可以拦截原型方法的能力,但是可以用自定义的方法去覆盖原生的原型方法

可以用一个拦截器覆盖Array.prototype。之后,每当使用Array原型上的方法操作数组时,其实执行的都是拦截器中提供的方法,比如push方法。然后,在拦截器中使用原生Array的原型方法去操作数组。

2  拦截器 

拦截器其实就是一个和Array.prototype一样的Object,里面包含的属性一模一样,只不过这个Object中某些可以改变数组自身内容的方法是我们处理过的。

Array原型中可以改变数组自身内容的方法有7个,分别是push、pop、shift、unshift、splice、sort、reverse

const arrayProto = Array.prototype;
// 使用arrayMethods去覆盖Array.prototype
export const arrayMethods = Object.create(arrayProto);
[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach(
  (method) => {
    // 缓存原始方法
    const original = arrayProto[method];
    /**
     * 在arrayMethods上使用Object.defineProperty方法将那些可以改变数组自身
     * 内容的方法进行封装
     */
    Object.defineProperty(arrayMethods, method, {
      value: function mutator(...args) {
        /**
         * 当使用push方法时,其实带哦用的是arrayMethods.push,而arrayMethods.push是函数mutator,
         * 实际上执行的是mutator函数。
         * 在mutator中执行original来做它应该做的事,比如push的功能。
         */
        return original.apply(this, args);
      },
      enumerable: false,
      writable: true,
      configurable: true,
    });
  }
);

3  使用拦截器覆盖Array原型

有了拦截器之后,想要它生效,需要使用它去覆盖Array.prototype。但是又不能直接覆盖,会污染全局的Array。我们只希望拦截器只覆盖响应式数组的原型

export class Observer {
  constructor(value) {
    this.value = value;
    if (Array.isArray(value)) {
      /**
       * 它的作用是将拦截器(加工后具备拦截功能的arrayMethods)赋值给value.__proto__,
       * 通过__proto__可以巧妙的实现覆盖value原型的功能
       */
      value.__proto__ = arrayMethods; // 新增
    } else {
      this.walk(value);
    }
  }
}

4  将拦截器方法挂载到数组的属性上

如果不能使用__proto__,就直接将arrayMethods身上的这些方法设置到被侦测的数组上;

import { arrayMethods } from "./array";
// __proto__是否可用
const hasProto = "__proto__" in {};
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
export class Observer {
  constructor(value) {
    this.value = value;
    if (Array.isArray(value)) {
      // 修改
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
    } else {
      this.walk(value);
    }
    // ....
  }
}
function protoAugment(target, src, keys) {
  // 覆盖原型
  target.__proto__ = src;
}
function copyAugment(target, src, keys) {
  //   将已经加工了拦截操作的原型方法直接添加到value的属性中
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i];
    def(target, key, src[key]);
  }
}

 5  如何收集依赖

 Object的依赖是在defineReactive中的getter里使用Dep收集的,每个key都会有一个对应的Dep列表来存储依赖。

Array的依赖和Object一样,也在defineReactive中收集:


function defineReactive(data, key, val) {
  if (typeof val === "object") new Observer(val);
  let dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend();
      // 这里收集Array的依赖
      return val;
    },
    set: function () {
      if (val === newVal) {
        return;
      }
      dep.notify();
      val = newVal;
    },
  });
}

所以,Array在getter中收集依赖,在拦截器中触发依赖

6  依赖列表存在哪儿

vue.js把Array的依赖存放在Observer中:

export class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep(); // 新增dep

    if (Array.isArray(value)) {
      // ....
    }
    // ...
  }
}

为什么数组的dep(依赖)要保存在Observer实例上呢?

前面介绍了数组在getter中收集依赖,在拦截器中触发依赖,所以这个依赖保存的位置很关键,它必须在getter和拦截器中都可以访问到。

我们之所以将依赖保存在Observer实例上,是因为在getter中可以访问到Observer实例,同时在Array拦截器中也可以访问到Observer实例。

7  收集依赖

将Dep实例保存在Observer的属性上之后,我们可以把getter中像下面这样访问并收集依赖:


function defineReactive(data, key, val) {
  let childOb = observe(val); // 修改
  let dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend();
      //   新增
      if (childOb) {
        childOb.dep.depend();
      }
      return val;
    },
    set: function () {
      if (val === newVal) {
        return;
      }
      dep.notify();
      val = newVal;
    },
  });
}
/**
 * 尝试为value创建一个Observer实例,
 * 如果创建成功,直接返回新创建的Observer实例,
 * 如果value已经存在一个Observer实例,则直接返回它
 */
export function observe(value, asRootData) {
  //   在defineReactive函数中调用了observe,它把val当作参数传了进去并拿到一个返回值,那就是Observer实例
  if (!isObject(value)) {
    return;
  }
  let ob;
  //   如果value已经是响应式数据,不需要再次创建Observer实例,
  //   直接返回已经创建的Observer实例即可,避免了重复侦测value变化的问题
  if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else {
    ob = new Observer(value);
  }
  return ob;
}

通过observe得到了数组的Observer实例(childOb),最后通过childOb的dep执行depend方法来收集依赖。

8  在拦截器中获取Observer实例

因为Array拦截器是对原型的一种封装,所以可以在拦截器中访问到this(当前正在被操作的数组)。


function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true,
  });
}
export class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
    // 在value上新增一个不可枚举的属性__ob__,这个属性的值就是当前Observer的实例
    def(value, "__ob__", this); //新增
    if (Array.isArray(value)) {
      // ....
    }
    // ....
  }
}

这样我们就可以通过数组数据的__ob__属性拿到Observer实例,然后就可以拿到__ob__上的dep了。

当然,__ob__的作用不仅仅是为了在拦截器中访问Observer实例那么简单,还可以用来标识当前value是否已经被Observer转换成了响应式数据

也就是说,所有被侦测了变化的数据身上都会有一个__ob__属性来表示它们是响应式的。

当value身上被标记了__ob__之后,就可以通过value.__ob__来访问Observer实例。如果是Array拦截器,因为拦截器是原型方法,所以可以直接通过this.__ob__来访问Observer实例。具体实现如下:

[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach(
  (method) => {
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
      value: function mutator(...args) {
        const ob = this.__ob__; // 新增
        return original.apply(this, args);
      },
      enumerable: false,
      writable: true,
      configurable: true,
    });
  }
);

9  向数组的依赖发送通知

前面介绍了如何在拦截器中访问Observer实例,所以这里只需要在Observer实例中拿到dep属性,就可以直接发送通知了:

[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach(
  (method) => {
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
      value: function mutator(...args) {
        const ob = this.__ob__; 
        ob.dep.notify(); // 向依赖发送消息
        return original.apply(this, args);
      },
      enumerable: false,
      writable: true,
      configurable: true,
    });
  }
);

10  侦测数组中元素的变化

其中数组中保存了一些元素,它们的变化也是需要侦测的。也就是说,所有响应式数据的组数据都要侦测,不论是Object中的数据还是Array中的数据(数组中的每一项都需要转换成响应式的)。

所有Observer类不光要处理Object类型,还要处理Array类型。

export class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
    def(value, "__ob__", this);
    // 新增
    if (Array.isArray(value)) {
      this.observeArray(value);
    } else {
      this.walk(value);
    }
    // ....
  }
  // 侦测Array中的每一项,执行observe函数来侦测变化
  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }
  // ...
}

11  侦测新增元素的变化

如果是push、unshift、splice新增数组元素,同时也要将内容转换成响应式的,拦截器中对数组方法的类型进行判断,然后使用Obverser来侦测它们就行。

[("push", "pop", "shift", "unshift", "splice", "sort", "reverse")].forEach(
  (method) => {
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
      value: function mutator(...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); //新增
        ob.dep.notify();
        return original.apply(this, args);
      },
      enumerable: false,
      writable: true,
      configurable: true,
    });
  }
);

12  关于Array的问题

this.list[0] = 2 或this.list.length = 0 都是无法监听到的,不会触发re-render或watch。

13  总结

Array跟踪变化是通过创建拦截器去覆盖数组原型的方法来跟踪。

为了不污染全局的Array.prototype,我们在Observer中只针对需要侦测变化的数组使用__proto__来覆盖原型方法。

Array和Object收集依赖的方式是一样的,都是在getter中收集。但是由于使用依赖位置的不同,数组要在拦截器中向依赖发消息,所以依赖不能像Object那样保存在defineReactive中,而是把依赖保存在了Observer实例上。

在Observer中,每个侦测了变化的数据都标上了__ob__,并把this(Observer实例)保存在__ob__上。主要两个作用,一是为了标记数据是否被侦测了变化,二是方便通过数据取到__ob__,从而拿到Observer实例上保存的依赖。

除了侦测数组自己的变化外,数组中元素发生的变化也要侦测。在Observer中判断如果当前被侦测的数据是数组,则调用observeArray方法将数组中的每一项都转换成响应式的并侦测变化。

除了侦测已有数据以外,当用户使用push等方法添加新数据时,新增的数据也要进行变化侦测。在拦截器方法中,判断如果是push、unshift、splice方法,则从参数中将新增数据提取出来,然后使用observeArray对新增数据进行变化侦测。

注:本文章来自于《深入浅出vue.js》(人民邮电出版社)阅读后的笔记整理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值