响应式原理

回顾下 2.0 响应式原理实现过程:

  1. Vue 在初始化的时候会遍历 data 数据,对于每个属性执行 defineReactive 方法,这里方法主要做以下两个事情:

    • 实例化一个事件器 Dep

    • 使用 Obejct.definedPropery() 方法代理属性并添加 set 和 get 方法, get 中添加了收集 Watcher 的逻辑,而 set 则是包含了执行 Watcher 的逻辑

  2. 在组件渲染的时候,会实例化一个 Watcher 实例,并将当前 Watcher 实例到全局属性 Dep.target中,之后在执行渲染操作的时候会读取 data 的属性, 并被 get 劫持,get 方法中将保存在全局的 Watcher 收集到 dep 中

  3. 之后当 data 属性被更改时,会被 set 劫持,然后执行 Watcher 中的更新方法

Vue3.0

3.0 中改用了 Proxy 方法,以 data 属性为例来品一下怎么玩的

源码追踪: mountComponent(n2, container) => setupComponent(instance) => setupStatefulComponent() => finishComponentSetup(instance) => applyOptions(instance, Component) => resolveData(instance, dataOptions, publicThis)

resolveData 就是处理 data 属性的地方

  function resolveData(instance, dataFn, publicThis) {
    // data属性只能是函数 
    if ( !isFunction(dataFn)) {
      warn(`The data option must be a function. ` +
        `Plain object usage is no longer supported.`);
    }
    // 获取 data 的值
    const data = dataFn.call(publicThis, publicThis);
    // data函数返回值不能是 Promise
    if ( isPromise(data)) {
      warn(`data() returned a Promise - note data() cannot be async; If you ` +
        `intend to perform data fetching before component renders, use ` +
        `async setup() + <Suspense>.`);
    }
    // data函数返回值必需是对象
    if (!isObject(data)) {
      warn(`data() should return an object.`);
    }
    else if (instance.data === EMPTY_OBJ) {
      // 设置代理
      instance.data = reactive(data);
    }
    else {
      // existing data: this is a mixin or extends.
      extend(instance.data, data);
    }
  }

上文对 data 属性和返回值做了一些校验后,执行 reactive(data) 方法设置代理

  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);
  }
  // reactive 实际上执行的是 `createReactiveObject` 方法

  function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
    if (!isObject(target)) {
      {
        console.warn(`value cannot be made reactive: ${String(target)}`);
      }
      return target;
    }
    // target is already a Proxy, return it.
    // exception: calling readonly() on a reactive object
    if (target["__v_raw" /* RAW */] &&
      !(isReadonly && target["__v_isReactive" /* IS_REACTIVE */])) {
      return target;
    }
    // target already has corresponding Proxy
   // 使用 new WeakMap() 保存当前代理
    const proxyMap = isReadonly ? readonlyMap : reactiveMap;
    
    const existingProxy = proxyMap.get(target);
    // 如果已经存在则是直接返回
    if (existingProxy) {
      return existingProxy;
    }
    // only a whitelist of value types can be observed.
    const targetType = getTargetType(target);
    if (targetType === 0 /* INVALID */) {
      return target;
    }
    const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
    proxyMap.set(target, proxy);
    return proxy;
  }

设置代理的地方: const proxy = new Proxy(target, baseHandlers), baseHandlers 就是 Proxy 的处理器对象,根据上下文找到 baseHandlers 的定义:

  const mutableHandlers = {
    get,
    set,
    deleteProperty,
    has,
    ownKeys
  }
  const get = /*#__PURE__*/ createGetter();

  function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) { // 设置 get 方法
      // 如果当前访问 __v_isReactive 返回 true
      if (key === "__v_isReactive" /* IS_REACTIVE */) {
        return !isReadonly;
      }
      // 如果当前访问 __v_isReadonly 返回 false
      else if (key === "__v_isReadonly" /* IS_READONLY */) {
        return isReadonly;
      }
      // 如果当前访问 __v_raw  从缓存中找对应的值,先不关心这个上
      else if (key === "__v_raw" /* RAW */ &&
        receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)) {
        return target;
      }
      // 如果当前访问的数组中的属性,且是 ['includes', 'indexOf', 'lastIndexOf'] 中的一种,则调用重写的方法
      const targetIsArray = isArray(target);
      if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
        return Reflect.get(arrayInstrumentations, key, receiver);
      }
      // 获取属性对应的值
      const res = Reflect.get(target, key, receiver);
      if (isSymbol(key)
        ? builtInSymbols.has(key)
        : key === `__proto__` || key === `__v_isRef`) {
        return res;
      }
      if (!isReadonly) {
        // 执行 track 方法,收集 effect 事件方法
        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;
    };
  }
 const set = /*#__PURE__*/ createSetter()
  function createSetter(shallow = false) {
    return function set(target, key, value, receiver) {
      const oldValue = target[key]; // 得到旧的值
      if (!shallow) {
        value = toRaw(value);
        if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
          oldValue.value = value;
          return true;
        }
      }
      const hadKey = isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key);
      const result = Reflect.set(target, key, value, receiver);
      // don't trigger if target is something up in the prototype chain of original
      if (target === toRaw(receiver)) {
        if (!hadKey) {
          trigger(target, "add" /* ADD */, key, value);
        }
        else if (hasChanged(value, oldValue)) {
          // 触发收集的事件,进行组件更新
          trigger(target, "set" /* SET */, key, value, oldValue);
        }
      }
      return result;
    };
  }

过程分析

收集依赖

当组件访问 data 中的属性时被 get 方法劫持,执行下面语句:

const res = Reflect.get(target, key, receiver)
if (!isReadonly) {
  track(target, "get" /* GET */, key);
}
// target 代理对象 属性
// type get
// key 要访问的属性
  function track(target, type, key) {
    if (!shouldTrack || activeEffect === undefined) {
      return;
    }
    // 通过当前对象获取 dep
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      // 如果没有就添加
      targetMap.set(target, (depsMap = new Map()));
    }
    // 通过属性获取 dep
    let dep = depsMap.get(key);
    if (!dep) {
      // 如果没有添加一个 dep
      depsMap.set(key, (dep = new Set()));
    }
    // 当前 dep 不包含 activeEffect 则添加到 dep 中
    if (!dep.has(activeEffect)) {
      dep.add(activeEffect);
      // 当前的dep 收集到activeEffect.deps 中
      activeEffect.deps.push(dep);
      if ( activeEffect.options.onTrack) {
        activeEffect.options.onTrack({
          effect: activeEffect,
          target,
          type,
          key
        });
      }
    }
  }

上面代码可以看到的信息:

  • 属性被访问时,会执行 track 方法做事件收集工作

  • activeEffect 是一个全局属性,记录当前正被执行的 effect, 其实这个 effect 对应 vue2.0 中的 Watcher

  • 每个代理对象都通过 WeakMap 结构保存着这个对象的 收集器集合 depsMap

  • 每个对象的属性再通过一步中的 depsMap 保存着对应的 dep, depsMap 是一个 Map 结构

以上就完成的事件收集的工作

触发更新

当更新响应的数据时,将被 set 方法劫持, 执行 set 中的 trigger 方法

 trigger(target, "set" /* SET */, key, value, oldValue);
  function trigger(target, type, key, newValue, oldValue, oldTarget) {
    // 通过对象获取当前 收集器集合 depMap
    const depsMap = targetMap.get(target);
    if (!depsMap) { // 如果没有,说明对象没有添加响应机制
      // never been tracked
      return;
    }
    const effects = new Set(); // 保存当前收集到且要执行的 effect 
    const add = (effectsToAdd) => {
      // 将收集到的 effect 添加到 effects 集合中
      if (effectsToAdd) {
        effectsToAdd.forEach(effect => {
          if (effect !== activeEffect || effect.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 {
      // 改变属性时,将执行下面语句
      if (key !== void 0) {
        // 根据 key 获取收集到的 dep 然后执行 add 方法
        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) {
      // 执行每个 effects 的scheduler 方法
        effect.options.scheduler(effect);
      }
      else {
        effect();
      }
    };
    // 遍历 effects 执行 run 方法
    effects.forEach(run);
  }

effect.options.scheduler 就是下面的 queueJob 方法

  function queueJob(job) {
    if ((!queue.length ||
      !queue.includes(job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)) &&
      job !== currentPreFlushParentJob) {
      queue.push(job);
      queueFlush();
    }
  }

queueJob 方法就是将当前 job(effect) 添加到 queue 队列中,然后执行 queueFlush 方法开始一次微任务的添加,这样在事件循环处理事件队列时就可以执行 effect 方法,具体执行的代码为:

  function queueFlush() {
    if (!isFlushing && !isFlushPending) {
      isFlushPending = true;
      currentFlushPromise = resolvedPromise.then(flushJobs);
    }
  }
  function flushJobs(seen) {
    isFlushPending = false;
    isFlushing = true;
    {
      seen = seen || new Map();
    }
    flushPreFlushCbs(seen);
    // 对收集到的 effect 进行排序
    queue.sort((a, b) => getId(a) - getId(b));
    try {
      for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
        const job = queue[flushIndex];
        if (job) {
          if (true) {
            checkRecursiveUpdates(seen, job);
          }
          // 调用 callWithErrorHandling 执行每一个 effect
          callWithErrorHandling(job, null, 14 /* SCHEDULER */);
        }
      }
    }
    finally {
      flushIndex = 0;
      queue.length = 0;
      flushPostFlushCbs(seen);
      isFlushing = false;
      currentFlushPromise = null;
      // some postFlushCb queued jobs!
      // keep flushing until it drains.
      if (queue.length || pendingPostFlushCbs.length) {
        flushJobs(seen);
      }
    }
  }

 // 通过 callWithErrorHandling 执行每个 effect
  function callWithErrorHandling(fn, instance, type, args) {
    let res;
    try {
      // 执行每一个 effect 方法
      res = args ? fn(...args) : fn();
    }
    catch (err) {
      handleError(err, instance, type);
    }
    return res;
  }

更新依赖的过程总结如下:

  1. 属性变更时,触发 trigger 方法,注意这个时间的 type=set

  2. 从 depsMap 取出当前属性收集的 effect 集合

  3. 遍历这些 effect ,执行每个 effect.options.scheduler(effect) 方法,将 efftct 添加到 queue 中

  4. 同时添加一个微任务到队列当中

  5. 执行微任务时,对 queue 中的 effect 进行排序,然后分别执行 effect 方法

关于深度对象的监听

Counter: {{ counter.cou }}
data() {
    return {
      counter: {
        cou: 1
      }
    }
  },

此时的处理步骤为:

  • 首先仍然会通过 resolveData(instance, dataOptions, publicThis) =>reactive(data) => createReactiveObject, 给当前 data 的值做 proxy 代理

  • 渲染组件访问 data 对象中的 counter 的属性,调用 track(target, "get" /* GET */, key),给当前属性 counter 添加事件 effect

  • 如果判断到 counter 属性的值是个对象,执行 isReadonly ? readonly(res) : reactive(res),相当于回到第一步,给当前 counter 值做 proxy 代理

  • 之后访问 counter 对象中访问 cou 的属性,又被 get 劫持,调用 track(target, "get" /* GET */, key),给当前属性 cou 添加事件 effect

可以看到对于深层的对象,仍然是需要对子对象做代理的,跟 vue2.0 一样,只是设置的代理的时机不同了

  • vue2.0 是初始化组件的时候遍历 data 及子属性对象,添加 Object.defineProperry 代理

  • vue3.0 只有 data 属性是在一开始的时候做 proxy 代理,之后是触发 get 后,再对子对象添加 proxy 代理

加个 proxy 粟子理解一下:

var handler = {
    get: function(obj, prop) {
        console.log(obj)
        return obj[prop]
    }
};

var p = new Proxy({a: {b: 34}}, handler)
//输入 p.a
//触发 console.log => - {a: {b: 34}}
//输出 {a: {b: 34}}

//输入 p.a.b
//触发 console.log => - {a: {b: 34}}
//输出 34

上面例子说明 prosy 只能代理第一层的属性

如果是添加属性呢?

假设我们给上面粟子中的 counter 对象在后期添加一个属性,Vue 又是怎么处理的呢?

this.counter.abc = 3

执行上面的语句时代码运行过程:

首先得访问 this.counter 属性,此时也会被 get 方法劫持,并执行 track(target, "get" /* GET */, key); 方法,但因为此时全局环境没有 activeEffect 事件,所以没有执行任何事件收集的动作

function track(target, type, key) {
  if (!shouldTrack || activeEffect === undefined) {
    return;
  }
}

然后 this.counter.abc = 3 时被 set 方法劫持

function set(target, key, value, receiver) {
  // 取旧值为 undefined
  const oldValue = target[key]; 
  if (!shallow) {
    value = toRaw(value); // 当前例子为 3 
    if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
      oldValue.value = value;
      return true;
    }
  }
  // 因为之前这个属性,hadKey 为false
  const hadKey = isArray(target) && isIntegerKey(key)
    ? Number(key) < target.length
    : hasOwn(target, key);
  // 给 target 添加这个属性
  const result = Reflect.set(target, key, value, receiver);
  if (target === toRaw(receiver)) {
    // hadKey 为 false,所以执行 trigger 方法
    if (!hadKey) {
      trigger(target, "add" /* ADD */, key, value);
    }
    else if (hasChanged(value, oldValue)) {
      trigger(target, "set" /* SET */, key, value, oldValue);
    }
  }
  return result;
};
 // 注意此时的 type = add
  function trigger(target, type, key, newValue, oldValue, oldTarget) {
    // 获取 target 收集器集合
    const depsMap = targetMap.get(target);
    if (!depsMap) {
      // never been tracked
      return;
    }
    const effects = new Set();
    const add = (effectsToAdd) => {
      if (effectsToAdd) {
        effectsToAdd.forEach(effect => {
          if (effect !== activeEffect || effect.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 {
      // key 有效
      if (key !== void 0) {
        // 但是因为当前 key 是新添加的,所以没有当前属性对应的 dep,我们继续往下走
        add(depsMap.get(key));
      }
      switch (type) {
        case "add" /* ADD */:
        // 当前 type 为 add 所以执行以下语句
          if (!isArray(target)) {
            // 取出 ITERATE_KEY 收集的 effect,之后的步骤添加到微任务队列触发 effect 事件,进行更新 
            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.forEach(run);
  }

总结一下当给对象添加一个属性时的大致过程:

  1. 首先给当前 target 添加新的属性并赋值

  2. 调用trigger(target, "add" /* ADD */, key, value);,从 ITERATE_KEY 中取出 effect 事件加入到队列

ITERATE_KEY 是通过 proxy 劫持 ownKeys 属性添加的事件收集

  function ownKeys(target) {
    debugger
    track(target, "iterate" /* ITERATE */, isArray(target) ? 'length' : ITERATE_KEY);
    return Reflect.ownKeys(target);
  }

如果是对象 ITERATE_KEY 属性, 数组的话就添加 length 属性,至少是怎么触发的 ownKeys 方法 没找到位置...

  1. 当组件更新的时候就会重新取新增的属性,并给属性添加 effect 事件收集

数组类型的更新

将上面的例子改成数据类型: counter: [1,2,3],当我们使用 counter.push(4) 时看下代码的运行

return function get(target, key, receiver) {
  // 略
  const targetIsArray = isArray(target);
  if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
    return Reflect.get(arrayInstrumentations, key, receiver);
  }
  const res = Reflect.get(target, key, receiver);
  // 略
  return res;
};

// arrayInstrumentations 方法定义
  const arrayInstrumentations = {};
  ['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
    const method = Array.prototype[key];
    arrayInstrumentations[key] = function (...args) {
      const arr = toRaw(this);
      for (let i = 0, l = this.length; i < l; i++) {
        track(arr, "get" /* GET */, i + '');
      }
      // we run the method using the original args first (which may be reactive)
      const res = method.apply(arr, args);
      if (res === -1 || res === false) {
        // if that didn't work, run it again using raw values.
        return method.apply(arr, args.map(toRaw));
      }
      else {
        return res;
      }
    };
  });
  ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(key => {
    const method = Array.prototype[key];
    arrayInstrumentations[key] = function (...args) {
      pauseTracking();
      const res = method.apply(this, args);
      resetTracking();
      return res;
    };
  });

首先当访问 push 属性时,也会被 proxy 劫持,根据下文中的代码,使用将会执行 return Reflect.get(arrayInstrumentations, key, receiver);

arrayInstrumentations 是重写后的一些数组方法的集合,所以将返回 arrayInstrumentations.push 方法

之后执行 push(4) 时, 将执行以下语句

  // 实际做的事情是  trackStack.push(false);  给 trackStack 添加一个 false 标识
  pauseTracking();
  // 执行真正的 Array.push 方法,此时应该等价于 counter[counter.length = 3] = 4 ,所以在获取属性 `3` 时, 会被 set 劫持
  const res = method.apply(this, args);
  resetTracking();
  return res;

上面代码主要做的事情

  1. pauseTracking():实际做的事情是 trackStack.push(false); 给 trackStack 添加一个 false 标识, 暂停依赖收集,也就意味在此期间执行 track 时,不会去收集依赖

      function track(target, type, key) {
        if (!shouldTrack || activeEffect === undefined) {
          return;
        }
      }
    
  2. 当执行 const res = method.apply(this, args), 会被 set 劫持

  function createSetter(shallow = false) {
    // 当前例子 key = 3
    // value = 4
    return function set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      trigger(target, "set" /* SET */, key, value, oldValue);
      return result;
    };
  }

  function trigger(target, type, key, newValue, oldValue, oldTarget) {
    const depsMap = targetMap.get(target);
    const effects = new Set();
    const add = (effectsToAdd) => {
      if (effectsToAdd) {
        effectsToAdd.forEach(effect => {
          if (effect !== activeEffect || effect.allowRecurse) {
            effects.add(effect);
          }
        });
      }
    };
    if (type === "clear" /* CLEAR */) {
      depsMap.forEach(add);
    }
    else if (key === 'length' && isArray(target)) {
    }
    else {
      // also run for iteration key on ADD | DELETE | Map.SET
      switch (type) {
        case "add" /* ADD */:
          if (!isArray(target)) { }
          else if (isIntegerKey(key)) {
            // new index added to array -> length changes
            add(depsMap.get('length'));
          }
          break;
      }
    }
    effects.forEach(run);
  }

跟之前 set 流程类型类型,之后在 trigger 方法根据 length 取出当前对象收集的 effect,添加到微任务队列中进行视图更新

那么使用数组下标更改数组也是同理

总结

其实思路还是跟 vue2.0 的差不多

  1. Vue 在初始化的时候使用 proxy 方法代理,关于收集器两个版本的区别

    Vue2.0 会为每个属性实例化一个事件收集器 Dep

    Vue3.0 则通过全局变量 const targetMap = new WeakMap(); 来做事件收集容器,targetMap 的 key 是代理对象,value 是 depsMap(一个收集事件容器的容器)

    depsMap 是 new Map() 结构,代理对象的每个属性收集的事件将存储在这个 depsMap 中,所以找属性收集的事件时,先根据这个属性所属对象从 targetMap 找 depsMap, 在根据具体属性从 depsMap 找事件

  2. 在组件渲染的时候,会生成一个 effect 方法,并将当前 effect 保存到全局 activeEffect中,之后在执行渲染操作的时候会读取 data 的属性, 并被 get 劫持,get 方法中将保存在全局的 activeEffect 收集到 depsMap 中

  3. 之后当 data 属性被更改时,会被 set 劫持,从 depsMap 取出 effect 并执行

Last Updated:
Contributors: 156081289@qq.com