Array的变化侦测

5/9/2023 VueVue2源码

# 拦截器

之前我们是通过给 Object 上的属性添加 getter/setter 来实现侦测,但是由于数组可以调用其原型方法对数组进行改变,我们就无法监听数组的变化。

那么只要用户在使用数组方法时得到通知,可以实现类似 Object 的侦测效果,所以我们需要一个拦截器覆盖 Array.prototype,在我们对数组进行操作时,实际是执行拦截器上的方法实现一些操作,再在拦截器中调用这些数组原始方法

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);

// vue主要对这7个方法进行劫持,这也是为什么只有这7种方法改变数组才会触发响应的原因
["push", "pop", "shift", "unshift", "reverse", "splice", "sort"].forEach(
  function (method) {
    // 缓存原始方法
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
      value: function mutator(...args) {
        return original.apply(this, args);
      },
      enumerable: true,
      writable: true,
      configurable: true,
    });
  }
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

我们使用 arrayMethods 来继承 Array.prototype,它具备 Array.prototype 的所有功能,接着使用 Object.defineProperty 对 arrayMethods 上的 7 大方法进行封装,后面会将 arrayMethods 将 Array.property 覆盖,这样在调用 array.push 时实际调用的是 arrayMethods 上的 push 方法,也就完成了对数据的拦截。

# 使用拦截器覆盖

# 覆盖 Array 原型

定义好了拦截器之后,就需要使用它去覆盖 Array.prototype,但是我们又不能直接覆盖 Array,我们只需要将要监听的数据的原型覆盖就行

class Observer {
  constructor(value) {
    this.value = value;
    if (Array.isArray(value)) {
      value.__proto__ = arrayMethods; // 新增
    } else {
      this.walk(value);
    }
  }
}
1
2
3
4
5
6
7
8
9
10

将之前定义的拦截器,赋值给要监听数组的__proto__,就实现对对应数组的原型进行覆盖

图1 使用__proto__覆盖原型

# 直接将拦截器挂载在数组的属性上

由于不是所有的浏览器都支持__proto__,那么对于不支持的,可以直接挂在在数组上

import { arrayMethods } from "./array";

// 判断是否有__proto__属性
const hasProto = "__proto__" in {};
const arrayKeys = Object.getOwnPropertyNames(arrayMethods); // 获取其属性key数组

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) {
  for (let i = 0; i < keys.length; i++) {
    def(target, keys[i], src[keys[i]]);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

我们通过判断{}是否有__proto__属性来判断浏览器是否支持__proto__,如果支持就直接将其覆盖,如果不支持则使用copyAugment方法遍历 7 大方法,然后直接在要监听数组的属性定义方法。

图2 将拦截器挂载在数组属性上

# 收集依赖

上述中我们只实现了数组发生变化以后通过拦截器通知的功能,但是通知的对象却没有。

在 Object 时,我们通过通知 Dep 中收集的依赖 Watcher 来实现,同理数组也是一样,我们在读取数组时,其实也可以通过Object.defineProperty来设置 getter/setter,在 getter 中完成依赖的收集,在拦截器中来触发依赖。

我们还需要知道依赖收集列表存在哪,Vue 是将其存在了 Observer 中,因为我们需要在拦截器和 getter 中都能访问到 Dep

name

// Observer
class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
  }
}
function defineReactive(data, key, value) {
  const childOb = observe(value); // 修改
  const dep = new Dep()
  Object.defineProperty(data, key {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend();
      // 新增
      if (childOb) {
        // 数组的依赖收集
        childOb.dep.depend()
      }
      return val;
    },
    set: function (newValue) {
      if (value === newValue) return;
      dep.notify();
      val = newValue;
    }
  })
}
/**
 * 如果value已经存在Observer实例,说明已经被监听,就直接返回
 * 否则就直接创建新的Observer实例,避免重复侦测value的变化
 */
function observe(value) {
  if (!(value instanceof Object)) return;
  let ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer){
    // vue会将Observer的实例挂在到观察对象的__ob__属性上,就可以用来判断是否被侦测
    ob = value.__ob__
  } else {
    ob = new Observer(value);
  }
  return ob
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

新建 observe 函数来创建 Observer 实例,如果 value 已经存在 Observer 实例,说明已经被监听,就直接返回,否则就直接创建新的 Observer 实例,避免重复侦测 value 的变化。

通过访问Observer实例的dep的depend方法,来为数组进行依赖收集。

Last Updated: 1/21/2025, 10:16:53 AM