Vue3响应式数据创建流程

193 阅读8分钟

本文源码版本为vue 3.3.4,主要基于打包后的源码梳理,沉下心,忽略次要内容,小白也能读懂

1.第一步

调用createRef函数开始创建

function ref(value) {
  return createRef(value, false);
}

附:
// createRef第二个参数用于区分是否浅响应,默认为false,可以对比shallowRef实现
function shallowRef(value) {
  return createRef(value, true);
}
2.第二步

createRef函数的实现也很简单,判断目标值是否已经是一个响应式的ref对象,若是则直接返回原值,否则使用目标参数构造一个RefImpl

function createRef(rawValue, shallow) {
  if (isRef(rawValue)) {
    return rawValue;
  }
  return new RefImpl(rawValue, shallow);
}


附:
// isRef函数实现 __v_isRef在此处还不明确其具体用途,下一步可以看到相关实现 
function isRef(r) {
  return !!(r && r.__v_isRef === true);
}
3.第三步

下面来看RefImpl类的具体实现

class RefImpl {
  constructor(value, __v_isShallow) {
    this.__v_isShallow = __v_isShallow; // 数据是否浅层响应
    this.dep = void 0;  // 副作用桶
    this.__v_isRef = true; // 标记数据是响应式的ref对象(这也是isRef实现的依据)
    this._rawValue = __v_isShallow ? value : toRaw(value); // 记录数据原始值
    this._value = __v_isShallow ? value : toReactive(value); // 原值或响应式代理对象
  }
  get value() {
    trackRefValue(this);  // 收集副作用
    return this._value;
  }
  set value(newVal) {
    // newVal是否浅响应或只读代理对象
    const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
    // newVal若是浅响应或只读代理对象,则newVal不做处理,否则调用toRaw获取newVal的原始值
    newVal = useDirectValue ? newVal : toRaw(newVal);
    /** 
     *  比较新值(newVal)和当前记录的原始值,若发生改变则:
     *  更新当前记录的原始值_rawValue
     *  更新_value
     *  触发副作用 
     */
    if (shared.hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = useDirectValue ? newVal : toReactive(newVal);
      triggerRefValue(this, newVal);
    }
  }
}

附:
// toRaw实现 只要目标数据'__v_raw'属性存在值则递归调用 返回最后一次'__v_raw'属性取得的值
// 可以看到这里出现了新的__v_开头的属性,到此时其实我们并不明确其作用,但由于之前已经出现了同样开头的__v_isRef属性,我们可以结合其命名推断它的大概用途
// raw /rɔ:/ (信息)未经处理的,原始的
function toRaw(observed) {
  const raw = observed && observed["__v_raw"];
  return raw ? toRaw(raw) : observed;
}

// hasChanged实现 Object.is静态方法确定两个值是否为相同值
const hasChanged = (value, oldValue) => !Object.is(value, oldValue);

到这一步我们可以说一个响应式的ref对象已经创建完成了,对目标值.value的读写操作都由该响应式ref对象gettersetter实现,同时在gettersetter中分别实现了副作用的收集和触发。当然,我们可以看到在构造函数和setter中,对于非浅响应式数据,其_value均通过toReactive函数进行了创建,toReactive的实现非常简单。

// 判断目标数据是否是object类型,若是则调用reactive创建响应式对象,否则返回原数据
const toReactive = (value) => shared.isObject(value) ? reactive(value) : value;

附:
// isObject实现
const isObject = (val) => val !== null && typeof val === "object";

到了这里,我们可以清晰地知道使用ref创建响应式数据比reactive多了哪些步骤,且对于对象类型的数据,最终也是通过reactive实现响应式的。还在天天总结refreactive有什么不同的同学可以歇一歇了,这块的源代码是非常简单和清晰的。

4.reactive实现
  1. 先来看reactive源码

    // 首先判断目标数据是否已经是只读代理对象 若是则直接返回该只读代理对象 否则调用createReactiveObject创建响应式数据
    function reactive(target) {
      if (isReadonly(target)) {
        return target;
      }
      return createReactiveObject(
        target,
        false,
        mutableHandlers,
        mutableCollectionHandlers,
        reactiveMap
      );
    }
    
    附:
    // isReadonly实现 新的__v_开头的属性'__v_isReadonly'
    function isReadonly(value) {
      return !!(value && value["__v_isReadonly"]);
    }
    
  2. createReactiveObject实现

    function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) {
      // 判断目标数据是否是object类型,若不是则打印警告信息,返回目标数据;若是则继续执行
      if (!shared.isObject(target)) {
        {
          console.warn(`value cannot be made reactive: ${String(target)}`);
        }
        return target;
      }
      // 出现了新的__v_开头属性__v_isReactive,同样结合命名有个大概推测 先跳过
      if (target["__v_raw"] && !(isReadonly2 && target["__v_isReactive"])) {
        return target;
      }
      // 从proxyMap(此时为reactiveMap)中获取key为目标元素的数据
      const existingProxy = proxyMap.get(target);
      // 如果存在 则表示响应式数据(代理对象)已经创建 直接返回取得的代理对象;否则继续执行
      if (existingProxy) {
        return existingProxy;
      }
      // 获取目标数据在创建代理对象时应该使用的类型 若为0则是无效的,直接返回目标数据
      const targetType = getTargetType(target);
      if (targetType === 0 /* INVALID */) {
        return target;
      }
      // 使用Proxy构造目标数据的代理对象,根据targetType的不同分别使用collectionHandlers或baseHandlers作为代理配置
      const proxy = new Proxy(
        target,
        targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers
      );
      // 以目标数据为key,其代理对象为值,添加到proxyMap中(此时为reactiveMap)
      proxyMap.set(target, proxy);
      return proxy;
    }
    
    附:
    // 可以看到形参proxyMap在reactive中调用时,传入的实参为reactiveMap,reactiveMap的定义如下:
    const reactiveMap = /* @__PURE__ */ new WeakMap();
    
    // getTargetType实现 
    // 新的__v_开头属性__v_skip(skip 跳过,略过),暂时不明确其含义,跳过
    // 判断目标数据属性__v_skip的值是否为true或者目标数据是否可扩展,若value["__v_skip"]为true或不可扩展则返回0,否则从targetTypeMap中获取toRawType(value)返回的值
    // 默认情况下对象是可扩展的,可以通过Object.preventExtensions()、Object.seal()、Object.freeze()、Reflect.preventExtensions()中的任意一种方法将对象标记为不可扩展
    function getTargetType(value) {
      return value["__v_skip"] || !Object.isExtensible(value) ? 0 /* INVALID */ : targetTypeMap(shared.toRawType(value));
    }
    
    // targetTypeMap实现
    // 根据targetTypeMap的返回值我们不难发现createReactiveObject在构造代理对象时,Object和Array类型的数据使用了baseHandlers作为代理配置;而Map、Set、WeakMap、WeakSet则都使用了collectionHandlers作为代理配置
    function targetTypeMap(rawType) {
      switch (rawType) {
        case "Object":
        case "Array":
          return 1 /* COMMON */;
        case "Map":
        case "Set":
        case "WeakMap":
        case "WeakSet":
          return 2 /* COLLECTION */;
        default:
          return 0 /* INVALID */;
      }
    }
    
    // toRawType相关实现
    const objectToString = Object.prototype.toString;
    const toTypeString = (value) => objectToString.call(value);
    const toRawType = (value) => {
      return toTypeString(value).slice(8, -1);
    };
    
  3. 我们知道,Vue响应式的实现原理就是拦截对象的读取和设置操作,在对对象进行读取操作时收集相应的副作用,在对对象进行设置操作时,再将读取时收集的副作用取出来一一执行,这样就实现了响应式。

    // 以《Vue.js设计与实现》书中的原例说明:
    
    const obj = {  text: 'hello world' }
    function effect() {
        // effect执行时会读取obj.text
        document.body.innerText = obj.text
    }
    
    // 如果我们手动操作obj.text的值使其发生变化(例如obj.text = 'hello vue3'),effect函数能够重新执行,那么很明显body的innerText也同样会发生相应的变化,和我们修改后的obj.text值保持一致,这样的对象obj我们称之为响应式数据。
    

    副作用的收集和触发有兴趣的同学可以参考《Vue.js设计与实现》的第四章,再查看最新源码梳理其流程。

    抛开副作用的收集和触发,不难发现,响应式数据的另一个核心就是拦截读取和设置操作,对于不同类型的数据,有不同的内置操作方法,因此针对不同类型的数据,在创建代理时需要使用不同的代理配置,而又由于Proxy的局限性,在创建响应式数据时Reflect也是必不可少的。

    下表是Proxy的代理配置提供的方法名称(引用自Proxy和Reflect):

    内部方法 Handler 方法 何时触发
    [[Get]] get 读取属性
    [[Set]] set 写入属性
    [[HasProperty]] has in 操作符
    [[Delete]] deleteProperty delete 操作符
    [[Call]] apply 函数调用
    [[Construct]] construct new 操作符
    [[GetPrototypeOf]] getPrototypeOf Object.getPrototypeOf
    [[SetPrototypeOf]] setPrototypeOf Object.setPrototypeOf
    [[IsExtensible]] isExtensible Object.isExtensible
    [[PreventExtensions]] preventExtensions Object.preventExtensions
    [[DefineOwnProperty]] defineProperty Object.defineProperty, Object.defineProperties
    [[GetOwnProperty]] getOwnPropertyDescriptor Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries
    [[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object.keys/values/entries
  4. 对数据的拦截和设置操作作为响应式的核心之一,从实现层面来说是简单清晰的,但从实现细节来讲,是复杂而精细的。接下来我们仅以baseHandlers这一代理配置的get方法进行梳理。

    // 在createReactiveObject的实现中,对Object和Array类型使用了baseHandlers作为代理配置
    // 而reactive调用createReactiveObject时传递的实参为mutableHandlers
    
    // mutableHandlers定义
    const mutableHandlers = {
      get: get$1,
      set: set$1,
      deleteProperty,
      has: has$1,
      ownKeys
    };
    
    // 我们以继续看get方法的具体实现
    const get$1 = /* @__PURE__ */ createGetter();
    
    // 继续找到createGetter的实现
    function createGetter(isReadonly2 = false, shallow = false) {
      /**
       * @param target 目标对象
       * @param key 被获取的目标属性名
       * @param receiver Proxy或者继承Proxy的对象
       */
      return function get2(target, key, receiver) {
        // 目标属性为'__v_isReactive'时,返回!isReadonly2,此时为true
        if (key === "__v_isReactive") {
          return !isReadonly2;
        // 目标属性为'__v_isReadonly'时,返回isReadonly2,此时为false
        } else if (key === "__v_isReadonly") {
          return isReadonly2;
        // 目标属性为'__v_isShallow'时,返回shallow,此时为false        
        } else if (key === "__v_isShallow") {
          return shallow;
        // 此处的条件由多个三元运算符构成,根据是否只读、是否浅响应使用对应的WeakMap
        // 此处的WeakMap为reactiveMap
        // 然后从reactiveMap中取出目标对象的值,若值存在且等于receiver,说明代理对象已存在
        // 同时此时访问的目标属性为'__v_raw'时,就返回原目标对象
        } else if (key === "__v_raw" && receiver === (isReadonly2 ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap).get(target)) {
          return target;
        }
        
        /** 
         * 通过上面对各个属性的判断及返回值,结合命名,我想我们对【在前面跳过的几个以__v_开头的属性的其中一些】的作用应该比较清晰了
         * __v_isReactive:标识数据是否是一个响应式的reactive对象
         * __v_raw:返回一个响应式reactive对象的原始数据
         * 因此我们可以知道上文第三步中提到的toRaw其作用是获取目标数据的原始值
         * 在createReactiveObject中我们跳过的第二个if条件是判断目标数据是否已经是一个代理对象
         */
        
        // 判断目标对象是否数组
        const targetIsArray = shared.isArray(target);
        // 判断是否只读模式,若不是只读模式,则进入条件执行
        if (!isReadonly2) {
          // 对数组的一些特殊操作单独处理
          if (targetIsArray && shared.hasOwn(arrayInstrumentations, key)) {
            return Reflect.get(arrayInstrumentations, key, receiver);
          }
          // 对hasOwnProperty单独处理
          if (key === "hasOwnProperty") {
            return hasOwnProperty;
          }
        }
        // 获取目标对象中属性为key的值
        const res = Reflect.get(target, key, receiver);
        // 判断目标属性是否是内置symbol拥有的键或者是不需要追踪的键,若是,直接返回上一步取得的结果res
        if (shared.isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
          return res;
        }
        // 如果不是只读模式,调用track收集对应的副作用
        if (!isReadonly2) {
          track(target, "get", key);
        }
        // 如果是浅响应 直接返回res
        if (shallow) {
          return res;
        }
        // 如果res已经是一个响应式ref对象,且目标对象是数组、目标属性是整数,则直接返回res对象;否则解包res返回其值
        if (isRef(res)) {
          return targetIsArray && shared.isIntegerKey(key) ? res : res.value;
        }
        // 如果res是对象,返回res的代理对象,根据是否只读模式调用readonly或reactive来创建
        if (shared.isObject(res)) {
          return isReadonly2 ? readonly(res) : reactive(res);
        }
        // 未触发以上情况 直接返回res
        return res;
      };
    }
    
    
    附:
    const reactiveMap = /* @__PURE__ */ new WeakMap();
    const shallowReactiveMap = /* @__PURE__ */ new WeakMap();
    const readonlyMap = /* @__PURE__ */ new WeakMap();
    const shallowReadonlyMap = /* @__PURE__ */ new WeakMap();
    
    // builtInSymbols实现 可以替换shared.isSymbol为具体实现在控制台运行查看
    const builtInSymbols = new Set(
      /* @__PURE__ */ Object.getOwnPropertyNames(Symbol).filter((key) => key !== "arguments" && key !== "caller").map((key) => Symbol[key]).filter(shared.isSymbol)
    );
    
    // isNonTrackableKeys实现
    const isNonTrackableKeys = /* @__PURE__ */ shared.makeMap(`__proto__,__v_isRef,__isVue`);
    function makeMap(str, expectsLowerCase) {
      const map = /* @__PURE__ */ Object.create(null);
      const list = str.split(",");
      for (let i = 0; i < list.length; i++) {
        map[list[i]] = true;
      }
      return expectsLowerCase ? (val) => !!map[val.toLowerCase()] : (val) => !!map[val];
    }
    
    // readonly代理实现
    function readonly(target) {
      return createReactiveObject(
        target,
        true,
        readonlyHandlers,
        readonlyCollectionHandlers,
        readonlyMap
      );
    }
    
    // arrayInstrumentations实现 对数组的特殊读取和隐式修改length属性的操作等进行单独处理
    const arrayInstrumentations = /* @__PURE__ */ createArrayInstrumentations();
    function createArrayInstrumentations() {
      const instrumentations = {};
      ["includes", "indexOf", "lastIndexOf"].forEach((key) => {
        instrumentations[key] = function(...args) {
          const arr = toRaw(this);
          for (let i = 0, l = this.length; i < l; i++) {
            track(arr, "get", i + "");
          }
          const res = arr[key](...args);
          if (res === -1 || res === false) {
            return arr[key](...args.map(toRaw));
          } else {
            return res;
          }
        };
      });
      ["push", "pop", "shift", "unshift", "splice"].forEach((key) => {
        instrumentations[key] = function(...args) {
          pauseTracking();
          const res = toRaw(this)[key].apply(this, args);
          resetTracking();
          return res;
        };
      });
      return instrumentations;
    }
    
    // hasOwnProperty实现
    // 获取原数据 收集副作用 使用原数据调用hasOwnProperty检查key是否存在
    // 此处若通过代理对象调用hasOwnProperty将发生循环调用,造成栈溢出或无限循环
    function hasOwnProperty(key) {
      const obj = toRaw(this);
      track(obj, "has", key);
      return obj.hasOwnProperty(key);
    }
    
    // shared相关实现
    const hasOwnProperty = Object.prototype.hasOwnProperty;
    const hasOwn = (val, key) => hasOwnProperty.call(val, key);
    const isArray = Array.isArray;
    const isSymbol = (val) => typeof val === "symbol";
    const isString = (val) => typeof val === "string";
    const isIntegerKey = (key) => isString(key) && key !== "NaN" && key[0] !== "-" && "" + parseInt(key, 10) === key;
    

到了这里我们发现在以上的梳理过程中,有一个__v_skip属性的作用还不明确,实际上我们在vuejs/core的源码中可以找到答案。

// 简单来说__v_skip属性为true的对象,createReactiveObject会从getTargetType函数得到一个表示无效的值,然后直接返回原数据,而不会创建代理

/**
 * Marks an object so that it will never be converted to a proxy. Returns the
 * object itself.
 *
 * @example
 * ```js
 * const foo = markRaw({})
 * console.log(isReactive(reactive(foo))) // false
 *
 * // also works when nested inside other reactive objects
 * const bar = reactive({ foo })
 * console.log(isReactive(bar.foo)) // false
 * ```
 *
 * **Warning:** `markRaw()` together with the shallow APIs such as
 * {@link shallowReactive()} allow you to selectively opt-out of the default
 * deep reactive/readonly conversion and embed raw, non-proxied objects in your
 * state graph.
 *
 * @param value - The object to be marked as "raw".
 * @see {@link https://vuejs.org/api/reactivity-advanced.html#markraw}
 */
export function markRaw<T extends object>(value: T): Raw<T> {
  def(value, ReactiveFlags.SKIP, true)
  return value
}

附:
export const enum ReactiveFlags {
  SKIP = '__v_skip',
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  IS_SHALLOW = '__v_isShallow',
  RAW = '__v_raw'
}

对数据的拦截和代理的复杂、精细远不止上面代码所呈现出来的部分,仅以上面的get代理配置实现来说,我们仅仅是走了一遍它的实现,而没有完全探究每一步操作的原因,比如为什么用Reflect.get(target, key, receiver)来返回值,就涉及到访问器属性对普通属性访问这一情况的考虑,再比如我们通过in操作符,for ... in 循环读取对象也发生了读取操作,但创建代理时其代理配置只有 get,set,deleteProperty,has,ownKeys几项配置,这些读取操作是如何拦截追踪的,这又涉及到了ECMA规范。

有兴趣的同学可以去深入学习,over~