从vue3.0源码解析响应式实现原理

335 阅读12分钟

此次分析的源码版本为3.0.5

前置知识:

  • Set:类似于数组,但是成员的值都是唯一的(注:两个{}是不等的,两个NaN是相等的)、
  • WeakSet:类似Set,但是WeakSet的成员只能是对象,而不能是其他类型的值,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,WeakSet 不可遍历。
  • Map:它类似于对象,也是键值对的集合,区别于对象的的是对象的键只能是字符串,而Map的键可以是任何类型。
  • WeakMap:WeakMap结构与Map结构类似,也是用于生成键值对的集合。WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。WeakMap的键名所指向的对象,不计入垃圾回收机制。
  • Proxy:目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
  • Reflect:ES6 为了操作对象而提供的新 API。Reflect对象的方法与Proxy对象的方法一一对应
  • 设计模式——发布订阅模式

Object.defineProperty

讲vue3.0的响应式之前,先回顾一下vue2.0的响应式实现过程:通过Object.defineProperty api 劫持对象的属性的get和set操作,在get过程进行以来收集,在set过程进行派发通知更新视图,响应式 API 和组件更新的关系如图所示:

熟悉设计模式的一定很清楚发布订阅模式,这个过程通过Dep(发布者类)和Watcher(订阅者类)实现。

  • Dep 负责收集所有相关的的订阅者 Watcher ,具体谁不用管,具体有多少也不用管,只需要根据 target 指向的计算去收集订阅其消息的 Watcher 即可,然后做好消息发布 notify 即可。
  • Watcher 负责订阅 Dep ,并在订阅的时候让 Dep 进行收集,接收到 Dep 发布的消息时,做好其 update 操作即可。

用一张图可以直观地看清这个流程。

在对vue2.0响应式原理了解了之后,我们知道 Object.defineProperty API 的一些缺点:

  • 不能监听对象属性新增和删除;
  • 初始化阶段递归执行 Object.defineProperty 带来的性能负担。

Reactive API

Vue.js 3.0 为了解决 Object.defineProperty 的这些缺陷,使用 Proxy API 重写了响应式部分,并独立维护和发布整个 reactivity 库。

reactive

        function reactive(target) {
          // if trying to observe a readonly proxy, return the readonly version.
          if (target && target["__v_isReadonly" /* IS_READONLY */]) {
              return target;
          }
          return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers);
      }
      function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
          // 目标必须是对象或数组类型
          if (!isObject(target)) {
              {
                  console.warn(`value cannot be made reactive: ${String(target)}`);
              }
              return target;
          }
          // target 已经是 Proxy 对象,直接返回
          // 有个例外,如果是 readonly 作用于一个响应式对象,则继续
          if (target["__v_raw" /* RAW */] &&
              !(isReadonly && target["__v_isReactive" /* IS_REACTIVE */])) {
              return target;
          }
          // target 已经有对应的 Proxy 了
          const proxyMap = isReadonly ? readonlyMap : reactiveMap;
          const existingProxy = proxyMap.get(target);
          if (existingProxy) {
              return existingProxy;
          }
          // 只有在白名单里的数据类型才能变成响应式
          const targetType = getTargetType(target);
          if (targetType === 0 /* INVALID */) {
              return target;
          }
          // 利用 Proxy 创建响应式
          const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
          // 给原始数据打个标识,说明它已经变成响应式,并且有对应的 Proxy 了
          proxyMap.set(target, proxy);
          return proxy;
      }

可以看到,reactive 内部通过 createReactiveObject 函数把 target 变成了一个响应式对象。

在这个过程中,createReactiveObject 函数主要做了以下几件事情。

  1. 函数首先判断 target 是不是数组或者对象类型,如果不是则直接返回。所以原始数据 target 必须是对象或者数组。
  2. 如果对一个已经是响应式的对象再次执行 reactive,还应该返回这个响应式对象,
  3. 如果对同一个原始数据多次执行 reactive ,那么会返回相同的响应式对象。
  4. 使用 getTargetType 函数对 target对象做一进步限制
function getTargetType(value) {
      return value["__v_skip" /* SKIP */] || !Object.isExtensible(value)
          ? 0 /* INVALID */
          : targetTypeMap(toRawType(value));
  }

比如,带有 __v_skip 属性的对象、不能扩展的对象实例是不能变成响应式的。

  1. 通过 Proxy API 劫持 target 对象,把它变成响应式。需要注意的是,这里 Proxy 对应的处理器对象会根据数据类型的不同而不同,我们先分析基本数据类型的 Proxy 处理器对象,reactive 函数传入的 baseHandlers 值是 mutableHandlers。
  2. 给原始数据打个标识。

Vue.js 3.0 的 reactive API 就是通过 Proxy 劫持数据,而且由于 Proxy 劫持的是整个对象,所以我们可以检测到任何对对象的修改,弥补了 Object.defineProperty API 的不足。

Proxy 处理器对象 mutableHandlers 的实现:

  const mutableHandlers = {
      get,  // 访问对象属性会触发 get 函数;
      set,  // 设置对象属性会触发 set 函数;
      deleteProperty, // 删除对象属性会触发 deleteProperty 函数;
      has,  // in 操作符会触发 has 函数;
      ownKeys  // 通过 Object.getOwnPropertyNames 访问对象属性名会触发 ownKeys 函数。
  };

依赖收集

get 函数

依赖收集发生在数据访问的阶段,由于我们用 Proxy API 劫持了数据对象,所以当这个响应式对象属性被访问的时候就会执行 get 函数,定位到get函数,其实它是执行 createGetter 函数的返回值。

createGetter

function createGetter(isReadonly = false, shallow = false) {
      return function get(target, key, receiver) {
          if (key === "__v_isReactive" /* IS_REACTIVE */) {
              // 代理 observed.__v_isReactive
              return !isReadonly;
          }
          else if (key === "__v_isReadonly" /* IS_READONLY */) {
              // 代理 observed.__v_isReadonly
              return isReadonly;
          }
          else if (key === "__v_raw" /* RAW */ &&
              receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)) {
              // 代理 observed.__v_raw
              return target;
          }
          // 处理数组的响应式
          const targetIsArray = isArray(target);
          // arrayInstrumentations包含对数组一些方法修改的函数
          if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
              return Reflect.get(arrayInstrumentations, key, receiver);
          }
          // 求值
          const res = Reflect.get(target, key, receiver);
          // 内置 Symbol key 不需要依赖收集
          const keyIsSymbol = isSymbol(key);
          if (keyIsSymbol
              ? builtInSymbols.has(key)
              : key === `__proto__` || key === `__v_isRef`) {
              return res;
          }
          // 依赖收集
          if (!isReadonly) {
              track(target, "get" /* GET */, key);
          }
          if (shallow) {
              return res;
          }
          if (isRef(res)) {
              // ref unwrapping - does not apply for Array + integer key.
              const shouldUnwrap = !targetIsArray || !isIntegerKey(key);
              return shouldUnwrap ? res.value : res;
          }
          if (isObject(res)) {
              // Convert returned value into a proxy as well. we do the isObject check
              // here to avoid invalid value warning. Also need to lazy access readonly
              // and reactive here to avoid circular dependency.
              return isReadonly ? readonly(res) : reactive(res);
          }
          return res;
      };
  }

get 函数主要做了四件事情:
首先对特殊的 key 做了代理,接着通过 Reflect.get 方法求值,如果 target 是数组且 key 命中了 arrayInstrumentations,则执行对应的函数。

arrayInstrumentations

const arrayInstrumentations = {};
  ['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
      const method = Array.prototype[key];
      arrayInstrumentations[key] = function (...args) {
          // toRaw 可以把响应式对象转成原始数据
          const arr = toRaw(this);
          for (let i = 0, l = this.length; i < l; i++) {
              // 依赖收集,跟踪数组每个元素的变化。
              track(arr, "get" /* GET */, i + '');
          }
          // 先尝试用参数本身,可能是响应式数据
          const res = method.apply(arr, args);
          if (res === -1 || res === false) {
              // 如果失败,再尝试把参数转成原始数据
              return method.apply(arr, args.map(toRaw));
          }
          else {
              return res;
          }
      };
  });


当 target 是一个数组的时候,我们去访问 target.includes、target.indexOf 或者 target.lastIndexOf 就会执行 arrayInstrumentations 代理的函数,除了调用数组本身的方法求值外,还对数组每个元素做了依赖收集。因为一旦数组的元素被修改,数组的这几个 API 的返回结果都可能发生变化,所以我们需要跟踪数组每个元素的变化。

回到 get 函数,第三步就是通过 Reflect.get 求值,然后会执行 track 函数收集依赖。

函数最后会对计算的值 res 进行判断,如果它也是数组或对象,则递归执行 reactive 把 res 变成响应式对象。

这么做是因为 Proxy 劫持的是对象本身,并不能劫持子对象的变化,这点和 Object.defineProperty API 一致。但是 Object.defineProperty 是在初始化阶段,即定义劫持对象的时候就已经递归执行了,而 Proxy 是在对象属性被访问的时候才递归执行下一步 reactive,这其实是一种延时定义子对象响应式的实现,在性能上会有较大的提升。

track 函数

整个 get 函数最核心的部分其实是执行 track 函数收集依赖,

track

// 是否应该收集依赖
let shouldTrack = true
// 当前激活的 effect
let activeEffect
// 原始数据对象 map
const targetMap = new WeakMap()
function track(target, type, key) {
      if (!shouldTrack || activeEffect === undefined) {
          return;
      }
      let depsMap = targetMap.get(target);
      if (!depsMap) {
          // 每个 target 对应一个 depsMap
          targetMap.set(target, (depsMap = new Map()));
      }
      let dep = depsMap.get(key);
      if (!dep) {
          // 每个 key 对应一个 dep 集合
          depsMap.set(key, (dep = new Set()));
      }
      if (!dep.has(activeEffect)) {
          // 收集当前激活的 effect 作为依赖
          dep.add(activeEffect);
          // 当前激活的 effect 收集 dep 集合作为依赖
          activeEffect.deps.push(dep);
          if ( activeEffect.options.onTrack) {
              activeEffect.options.onTrack({
                  effect: activeEffect,
                  target,
                  type,
                  key
              });
          }
      }
  }

分析这个函数前,需要先想想收集的依赖是什么,我们的目的是实现响应式,就是当数据变化的时候可以自动做一些事情,比如执行某些函数,所以我们收集的依赖就是数据变化后执行的副作用函数

再来看实现,我们把 target 作为原始的数据,key 作为访问的属性。我们创建了全局的 targetMap 作为原始数据对象的 Map,它的键是 target,值是 depsMap,作为依赖的 Map;这个 depsMap 的键是 target 的 key,值是 dep 集合,dep 集合中存储的是依赖的副作用函数。为了方便理解,可以通过下图表示它们之间的关系:

所以每次 track ,就是把当前激活的副作用函数 activeEffect 作为依赖,然后收集到 target 相关的 depsMap 对应 key 下的依赖集合 dep 中。

派发通知

set 函数

派发通知发生在数据更新的阶段 ,由于我们用 Proxy API 劫持了数据对象,所以当这个响应式对象属性更新的时候就会执行 set 函数。我们来看一下 set 函数的实现,它是执行 createSetter 函数的返回值:

createSetter

function createSetter(shallow = false) {
      return function set(target, key, value, receiver) {
          const oldValue = target[key];
          if (!shallow) {
              // toRaw 可以把响应式对象转成原始数据
              value = toRaw(value);
              if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
                  oldValue.value = value;
                  return true;
              }
          }
          // 判断target是否存在这个key
          const hadKey = isArray(target) && isIntegerKey(key)
              ? Number(key) < target.length
              : hasOwn(target, key);
          const result = Reflect.set(target, key, value, receiver);
          // 如果目标的原型链也是一个 proxy,通过 Reflect.set 修改原型链上的属性会再次触发 setter,这种情况下就没必要触发两次 trigger 了
          if (target === toRaw(receiver)) {
              if (!hadKey) {
                  // 处理新增key
                  trigger(target, "add" /* ADD */, key, value);
              }
              else if (hasChanged(value, oldValue)) {
                  // 处理更新key
                  trigger(target, "set" /* SET */, key, value, oldValue);
              }
          }
          return result;
      };
  }

从代码里看,set主要做了两件事:

  1. 通过Reflect.set求值
  2. 通过trigger函数派发通知,并依据 key 是否存在于 target 上来确定通知类型,即新增还是修改。

trigger函数

整个 set 函数最核心的部分就是执行 trigger 函数派发通知

trigger

    function trigger(target, type, key, newValue, oldValue, oldTarget) {
      // 取对应target的副作用函数Map
      const depsMap = targetMap.get(target);
      if (!depsMap) {
          // never been tracked
          return;
      }
      // 创建运行的 effects 集合
      const effects = new Set();
      // 添加 effects 的函数
      const add = (effectsToAdd) => {
          if (effectsToAdd) {
              effectsToAdd.forEach(effect => {
                  if (effect !== activeEffect || effect.options.allowRecurse) {
                      effects.add(effect);
                  }
              });
          }
      };
      if (type === "clear" /* CLEAR */) {
          // collection being cleared
          // trigger all effects for target
          depsMap.forEach(add);
      }
      else if (key === 'length' && isArray(target)) {
          depsMap.forEach((dep, key) => {
              if (key === 'length' || key >= newValue) {
                  add(dep);
              }
          });
      }
      else {
          // SET | ADD | DELETE 操作之一,添加对应的 effects
          if (key !== void 0) {
              add(depsMap.get(key));
          }
          // also run for iteration key on ADD | DELETE | Map.SET
          switch (type) {
              case "add" /* ADD */:
                  if (!isArray(target)) {
                      add(depsMap.get(ITERATE_KEY));
                      if (isMap(target)) {
                          add(depsMap.get(MAP_KEY_ITERATE_KEY));
                      }
                  }
                  else if (isIntegerKey(key)) {
                      // new index added to array -> length changes
                      add(depsMap.get('length'));
                  }
                  break;
              case "delete" /* DELETE */:
                  if (!isArray(target)) {
                      add(depsMap.get(ITERATE_KEY));
                      if (isMap(target)) {
                          add(depsMap.get(MAP_KEY_ITERATE_KEY));
                      }
                  }
                  break;
              case "set" /* SET */:
                  if (isMap(target)) {
                      add(depsMap.get(ITERATE_KEY));
                  }
                  break;
          }
      }
      const run = (effect) => {
          if (effect.options.onTrigger) {
              effect.options.onTrigger({
                  effect,
                  target,
                  key,
                  type,
                  newValue,
                  oldValue,
                  oldTarget
              });
          }
          // 调度执行
          if (effect.options.scheduler) {
              effect.options.scheduler(effect);
          }
          else {
              // 直接运行
              effect();
          }
      };
      // 遍历执行 effects
      effects.forEach(run);
  }

trigger 函数的实现也很简单,主要做了四件事情:

  1. 通过 targetMap 拿到 target 对应的依赖集合 depsMap;
  2. 创建运行的 effects 集合;
  3. 根据 key 从 depsMap 中找到对应的 effects 添加到 effects 集合;
  4. 遍历 effects 执行相关的副作用函数。

所以每次 trigger 函数就是根据 target 和 key ,从 targetMap 中找到相关的所有副作用函数遍历执行一遍。

副作用函数收集过程

effect

  // 副作用函数栈
  const effectStack = [];
  // 当前激活的 effect
  let activeEffect;
  function effect(fn, options = EMPTY_OBJ) {
      if (isEffect(fn)) {
          // 如果 fn 已经是一个 effect 函数了,则指向原始函数
          fn = fn.raw;
      }
      // 创建一个 wrapper,它是一个响应式的副作用的函数
      const effect = createReactiveEffect(fn, options);
      // lazy 配置,计算属性会用到,非 lazy 则直接执行一次
      if (!options.lazy) {
          effect();
      }
      return effect;
  }
  function createReactiveEffect(fn, options) {
      const effect = function reactiveEffect() {
          // 非激活状态,则判断如果非调度执行,则直接执行原始函数。
          if (!effect.active) {
              return options.scheduler ? undefined : fn();
          }
          if (!effectStack.includes(effect)) {
              // 清空 effect 引用的依赖
              cleanup(effect);
              try {
                  // 开启全局 shouldTrack,允许依赖收集
                  enableTracking();
                  // 压栈
                  effectStack.push(effect);
                  activeEffect = effect;
                  // 执行原始函数
                  return fn();
              }
              finally {
                  // 出栈
                  effectStack.pop();
                  // 恢复 shouldTrack 开启之前的状态
                  resetTracking();
                  // 指向栈最后一个 effect
                  activeEffect = effectStack[effectStack.length - 1];
              }
          }
      };
      effect.id = uid++;
      // 标识是一个 effect 函数
      effect._isEffect = true;
      // effect 自身的状态
      effect.active = true;
      // 包装的原始函数
      effect.raw = fn;
      // effect 对应的依赖,双向指针,依赖包含对 effect 的引用,effect 也包含对依赖的引用
      effect.deps = [];
       // effect 的相关配置
      effect.options = options;
      return effect;
  }

effect 内部通过执行 createReactiveEffect 函数去创建一个新的 effect 函数,为了和外部的 effect 函数区分,我们把它称作 reactiveEffect 函数。这个 reactiveEffect

函数就是响应式的副作用函数,当执行 trigger 过程派发通知的时候,执行的 effect 就是它。

  1. 首先它会判断 effect 的状态是否是 active,这其实是一种控制手段,允许在非 active 状态且非调度执行情况,则直接执行原始函数 fn 并返回。
  2. 这个过程维护了一个副作用的函数栈effectStack,判断 effectStack 中是否包含 effect,如果没有就把 effect 压入栈内。

考虑到key属性的副作用可能是一个嵌套副作用函数,所以这里用一个栈结构来维护副作用函数栈。

  1. 在入栈前会执行 cleanup 函数清空 reactiveEffect 函数对应的依赖 。在执行 track 函数的时候,除了收集当前激活的 effect 作为依赖,还通过 activeEffect.deps.push(dep) 把 dep 作为 activeEffect 的依赖,这样在 cleanup 的时候我们就可以找到 effect 对应的 dep 了,然后把 effect 从这些 dep 中删除。

cleanup 函数的代码如下所示:

function cleanup(effect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

至此,我们从 reactive API 入手了解了整个响应式对象的实现原理。

通过这篇文章你可以掌握:

  • 明白响应式 API 的实现原理,
  • 什么时候收集依赖,什么时候派发更新,
  • 副作用函数的作用和设计原理。
  • 知道 reactive、readonly、ref 三种 API 的区别和各自的使用场景。

最后我们通过一张图来看一下整个响应式 API 实现和组件更新的关系:

它和前面 Vue.js 2.x 的响应式原理图很接近,其实 Vue.js 3.0 在响应式的实现思路和 Vue.js 2.x 差别并不大,主要就是 劫持数据的方式改成用 Proxy 实现 , 以及收集的依赖由 watcher 实例变成了组件副作用渲染函数 。