vue3-reactive源码解析

644 阅读13分钟

阅读准备

在阅读 reactive 源码之前,我们需要知道它的特性,了解特性推荐阅读单例测试源码或者是阅读官网的 API,推荐阅读单例,在后面阅读时才能更好理解。vue中响应式数据是通过Proxy来实现的,可以通过我之前写的Proxy 和 Reflect来了解它的特性和一些注意事项。

vue3中使用创建reactive类对象一共有四种api,分别对应不同的功能的对象

  • reactive创建可深入响应的可读写对象
  • readonly创建可深入响应的只读对象
  • shallowReactive创建只有表层(一层)的浅可读写对象
  • shallowReadonly创建只有表层(一层)的浅只读对象

使用reactivereadonly时会自动解构对象且不包括是数组子项的Ref对象,无论解构有多深,而 shallow类的对象则不会自动解构,只有一层也不会自动解构。比如

import { reactive, ref } from 'vue'

const observed1 = reactive({
  a: ref(1),
  b: {
    c: ref(1)
  },
  e: ref({ c: ref(1) })
  d: [ref(1), { a: ref(2)}] as const
})
const observed2 = reactive({
  d: [ref(1)] as const
})
observed2._a = ref({v: 1})

// Number 1
console.log(observed1.a)
// Number 1
console.log(observed1.b.c)
// Number 1
console.log(observed1.e.c)
// RefImpl value: Number 1
console.log(observed1.d[0])
// Number 1
console.log(observed1.d[1].a)
// RefImpl value: Number 1
console.log(observed2.d[0])
// Number 1
console.log(observed2._a.v)


const shallowObserved = shallowReactive({
  a: ref(1)
})
// RefImpl value: Number 1
console.log(observed.a)

reactive深入对象内部创建子代理时遇到下面这些情况不会创建

  • 不可更改属性描述符的对象不会创建,比如调用Object.prevenExtensions使对象不可扩展、Object.freeze冻结对象,Object.seal封闭对象
  • 在对象上声明__v_skip属性为true
  • 对象调用vuemarkRaw

reactive对象与readonly对象互相转换时,readonly对象不可转为reactivereactive可以转为readonly,但是转化的对象使用isReactiveisReadonly函数调用时都是返回true。比如下方代码:

import { reactive, readonly, isReactive, isReadonly } from "vue";

const are = reactive({ a: 2 });
const aro = readonly(are);
// true
console.log(isReactive(aro));
// true
console.log(isReadonly(aro));

const bre = readonly({ b: 2 });
const bro = reactive(bre);
// false
console.log(isReactive(bro));
// true
console.log(isReadonly(bro));

思考实现

  如果让我们基于Proxy来实现vue的响应式数据的话,我们会怎么设计呢,如果是我会这样设计,因为是响应式的,那么我们必须知道是哪些属性被使用,在它更改时需要重新执行监听函数,我们可以把监听函数跟收集函数统一使用一个函数来执行,而收集的函数里面收集到的key可能是动态的,所以每次更改都要清空依赖重新收集一遍依赖,加上Proxy的特性所以当收集函数使用get获取数据时收集当前使用key,当外部set修改数据时查看当前set是查看key是否被收集,如果被收集了,则set之后重新触发收集函数再次收集,而vue3中这个收集函数就是effect。当然真实使用时肯定不止getset这里为了方便理解,我们先按简单的看。逻辑如下图所示

WX20220209-223123@2x.png

  为了充分覆盖用户的数据,在Proxy获取的数据应该始终是Proxy,否则当用户修改里面的子对象时无法监听,比如下方这种情况

const rt = reactive({a: {b: 3}})
// 代理内部也应该将 rt.a 转化为代理
const ra = ra.a
effect(() => {
  // 收集到依赖
  console.log(ra.b)
})
// 触发重新执行effect
ra.b = 4

  还有一个原则,就是存储到原始数据时始终不应该存储Proxy后的数据,否则会造成混乱。

入口

  在vue3中创建(除去ref)中创建响应式对象一共有四种方法,分别是reactiveshallowReactivereadonlyshallowReadonly这四种方法的入口源码如下


// 创建reactive对象
export function reactive(target: object) {
  // 如果reactive进入的是readonly的话直接返回,保持只读
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  // 创建reactive对象
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

// shallowReactive创建,不会自动解包,只有根级别属性才是反应式的
export function shallowReactive<T extends object>(
  target: T
): ShallowReactive<T> {
  return createReactiveObject(
    target,
    false,
    shallowReactiveHandlers,
    shallowCollectionHandlers,
    shallowReactiveMap
  )
}

// 创建readonly代理,如果已经是reactive可以附加readonly标识
export function readonly<T extends object>(
  target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
  return createReactiveObject(
    target,
    true,
    readonlyHandlers,
    readonlyCollectionHandlers,
    readonlyMap
  )
}

export function shallowReadonly<T extends object>(target: T): Readonly<T> {
  return createReactiveObject(
    target,
    true,
    shallowReadonlyHandlers,
    shallowReadonlyCollectionHandlers,
    shallowReadonlyMap
  )
}

  在上面源码我们可以看到,这四种创建响应式对象最终都是使用createReactiveObject方法来实现的,只是参数不一样, 第一个参数是加工对象,第二个参数是标识只读和可读写,为true时为只读,第三四个参数是代理的拦截器,一个是常用类型拦截器,一个是集合类型拦截器,这里集合数据标识MapSetWeakMapWeakSet为什么将他们区分实现,可以查看之前我写的代理具有内部插槽的内建对象,而且集合类型是通过api来获取和存储数据的,并且存储时不会经过代理,代理只能拦截到获取api和特定的属性(如size)。最后一个参数就是每个代理类型(是否shallow(浅处理)和是否只读)的存储池了,里面存储了每个对象与代理的map关系。

  我们看看四种代理类型的 Map 声明:

// origin与proxy的映射
export const reactiveMap = new WeakMap<Target, any>()
export const shallowReactiveMap = new WeakMap<Target, any>()
export const readonlyMap = new WeakMap<Target, any>()
export const shallowReadonlyMap = new WeakMap<Target, any>()

  存储map都是用WeakMap当用户丢弃这个代理和代理对象时会自动进行内存回收,方便管理。 接下来我们看看真正的入口源码:

// 创建代理对象
function createReactiveObject(
  // 要代理的数据
  target: Target,
  // 是否是只读
  isReadonly: boolean,
  // 基础代理器
  baseHandlers: ProxyHandler<any>,
  // 集合代理器
  collectionHandlers: ProxyHandler<any>,
  // 映射map
  proxyMap: WeakMap<Target, any>
) {
  // 如果代理的数据不是obj则直接返回原对象
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`);
    }
    return target;
  }
  // 如果传入的已经是代理了,而且不是readonly -> reactive的转换则直接返回
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target;
  }

  // 查看当前origin对象之前是不是创建过当前代理,如果创建过直接返回之前缓存的代理对象
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  // 如果当前对象无法创建代理则直接返回origin
  const targetType = getTargetType(target);
  if (targetType === TargetType.INVALID) {
    return target;
  }

  // 查看当前origin type选择集合拦截器还是基础拦截器
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  );
  // 缓存
  proxyMap.set(target, proxy);
  return proxy;
}

  在createReactiveObject实现用到了ReactiveFlags,这些都是当前代理的特定标识,在后面代理具体实现中会附加上,稍后讲到具体实现时能看到,这些标识分别是:

// reactive 标志符常量
export const enum ReactiveFlags {
  // 是否阻止成为代理属性
  SKIP = '__v_skip',
  // 是否是reactive属性
  IS_REACTIVE = '__v_isReactive',
  // 是否是readonly属性
  IS_READONLY = '__v_isReadonly',
  // mark target
  RAW = '__v_raw'
}

  创建代理对象的Target必须为对象,不能为基础对象,reactive类型可以转化为readonly但是不能逆转,同一个对象不会创建两次,会在map中查看当前对象是否已经创建,如果创建了直接返回缓存中的。我们可以看到vue还会给每个对象打上TargetType类型,如果为INVALID的则标识为不可创建,直接返回源对象,TargetType的源码如下:

// reactive origin类型常量
const enum TargetType {
  // 无效的 比如基础数据类型
  INVALID = 0,
  // 常见的 比如object Array
  COMMON = 1,
  // 集合类型比如 map set
  COLLECTION = 2
}

// 获取origin 类型辅助函数
function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

const toRawType = (value: unknown): string => {
  // extract "RawType" from strings like "[object RawType]"
  return toTypeString(value).slice(8, -1)
}

// 获取origin的类型
function getTargetType(value: Target) {
  // 如果mark了不可reactive或者是不可扩展的直接返回无效
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}

  可以看到Vue中的Proxy对象不能创建原型被冻结的对象,这个很好理解,因为Vue需要对Target代理附加很多东西,原型被冻结将会附加失败。被用户主动mark的对象也不能创建。markRaw的源码如下:

// 记录对象不可代理
export function markRaw<T extends object>(value: T): T {
  def(value, ReactiveFlags.SKIP, true)
  return value
}

  createReactiveObject会根据getTargetType返回的数据类型来选择是使用collectionHandlers集合拦截器还是baseHandlers常用拦截器。创建完成后会将代理缓存起来,方便下次获取和查询。到这里入口就已经看完了。接下来我们看看一些辅助函数,这些函数比较简单这里就不再讲解了:

// 是否是reactive
export function isReactive(value: unknown): boolean {
  // 如果当前value是readonly则查看raw是不是reactive
  if (isReadonly(value)) {
    return isReactive((value as Target)[ReactiveFlags.RAW])
  }
  return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}

// 是否是readonly
export function isReadonly(value: unknown): boolean {
  return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}

// 查看当前是否是proxy,为reactive 或readonly则为内部代理
export function isProxy(value: unknown): boolean {
  return isReactive(value) || isReadonly(value)
}

// 获取数据原始值
export function toRaw<T>(observed: T): T {
  // 获取reactive原始对象,因为可能会有 readonly(reactive({}))写法,所以需要深入获取
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

// 将对象转化为reactive
export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

// 对象转化为readonly
export const toReadonly = <T extends unknown>(value: T): T =>
  isObject(value) ? readonly(value as Record<any, any>) : value

  在思考实现中我们讲到Proxy会收集和重新调用effect,在vue中做了进一步的细分,会用枚举区分因为什么操作收集,因为什么操作重新执行,定义的常量如下

// 因为什么收集
export const enum TrackOpTypes {
  // 如 observed.a
  GET = 'get',
  // 如 a in observed
  HAS = 'has',
  // 如 Object.keys(observed)
  ITERATE = 'iterate'
}

// 因为什么重新触发
export const enum TriggerOpTypes {
  // 如修改 observed.a = 1
  SET = 'set',
  // 如新增 observed.b = 3
  ADD = 'add',
  // 如 delete observed.a
  DELETE = 'delete',
  // 在集合时使用 如map.clear()
  CLEAR = 'clear'
}

  在vue中收集依赖统一使用track函数,重新触发effect是统一使用trigger函数,tracktrigger除了收集操作枚举外还需要传入key,操作什么key引起的收集和触发,如果是因为ownKey(如Object.key(...))引起的收集会使用一个内置的常量ITERATE_KEY替代key,关于tracktrigger里面具体实现我们讲到effect章节时再讲解,这里主要将代理的实现,使用方式如下:

// 在vue中除了`Map`的`keys`,其他迭代的都用这个symbol来代替key,
const ITERATE_KEY = Symbol(__DEV__ ? "iterate" : "");

// 收集什么变量,什么操作,收集到什么key
track(target, TrackOpTypes.ITERATE, ITERATE_KEY);
track(target, TrackOpTypes.GET, "a");
track(target, TrackOpTypes.HAS, "a");

// 什么变量引起触发,什么操作,变量什么key引起触发,新值、旧值(如果有)
trigger(target, TriggerOpTypes.ADD, key, value);
trigger(target, TriggerOpTypes.SET, key, value, oldValue);
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue);

常用拦截器

  在上面中我们看到常用拦截器对象有四种分别是mutableHandlersreadonlyHandlersshallowReactiveHandlersshallowReadonlyHandlers分别对应reactivereadonlyshallowReactiveshallowReadonly类型的代理,接下来我们看看源码实现:

// 创建get handler
const get = /*#__PURE__*/ createGetter();
// 创shallow的 get handler
const shallowGet = /*#__PURE__*/ createGetter(false, true);
// 创建readonly的 get Handler
const readonlyGet = /*#__PURE__*/ createGetter(true);
// 创建shallowReadonly的 get handler
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true);

const set = /*#__PURE__*/ createSetter();
const shallowSet = /*#__PURE__*/ createSetter(true);

// reactive拦截器
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys,
};

// readonly拦截器
export const readonlyHandlers: ProxyHandler<object> = {
  get: readonlyGet,
  set(target, key) {
    if (__DEV__) {
      console.warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      );
    }
    return true;
  },
  deleteProperty(target, key) {
    if (__DEV__) {
      console.warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target
      );
    }
    return true;
  },
};

// shallow reactive拦截器
export const shallowReactiveHandlers = /*#__PURE__*/ extend(
  {},
  mutableHandlers,
  {
    get: shallowGet,
    set: shallowSet,
  }
);

export const shallowReadonlyHandlers = /*#__PURE__*/ extend(
  {},
  readonlyHandlers,
  {
    get: shallowReadonlyGet,
  }
);

  拦截器的getset都是由createGettercreateSetter创建的,只是参数差异,在createGetter函数中第一个参数标识是否只读,第二个参数标识是否shallowcreateSetter不带是否只读因为,只读的Proxy不能set,只需要提示不能更改,同理has拦截器也是没必要添加。可能你会想只读的proxy按理来说也不需要get因为对象不会更改,也就不需要收集数据,对了一半,get中确实不会收集,但是vue中代理不仅需要在get拦截器上收集数据而且需要附加Flags和创建子对象proxy(如果不是shallow的话)

  在上面每个函数前都添加了/*#__PURE__*/这段注释的作用就是提示打包器这些变量时纯的,当没有使用到这些变量时可以剔除,减小打包后包的大小。

get 拦截器

  我们在思考实现中提到get拦截器大概职责是在effect时收集依赖,除此之外我们还需要保证Proxy get获取到的都是proxy,因为Proxy只是代理当前对象,子对象也需要深入创建,比如const oa = reactive({a: { b: 2 }})需要保证oa.a获取到的也是代理,接下来我们看createGetter源码:

// 不track收集和创建代理的的keys 包括原型,ref标识 vue标识
const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`)

// 获取所有内置的Symbol
const builtInSymbols = new Set(
  Object.getOwnPropertyNames(Symbol)
    .map(key => (Symbol as any)[key])
    .filter(isSymbol)
)

// 创建get handler
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // 附加flags 
    // 获取是否是获取当前是否是reactive | readonly
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      // 如果是获取源对象,通过代理和源数据WeakMap获取是否有被创建过
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      // 直接返回被代理对象
      return target
    }

    // 是否是数组
    const targetIsArray = isArray(target)

    // 如果当前数据是数组,而且是访问需要改造器后使用的,则使用改造器访问
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    // 如果当前key是内置symbol key或者是不需要处理的key则直接返回
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    // 如果不是只读的则track收集
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    // shallow类直接返回,不需要神UR创建
    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // 是否应该解包ref,如果是不是数组或者是数组但是访问非int key则应该解包
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    if (isObject(res)) {
      // 获取时才创建相对应类型的代理,将访问值也转化为reactive
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

  createGetter采用闭包包裹当前代理类型,当使用特定Flag属性获取时可以方便的返回当前代理的类型。当获取Proxy代理前数据时必须是已加是加工为代理的对象才能获取。

  当代理类型是只读时是不会track(收集)依赖的,因为只读代理不会更改,收集没意义。如果是shallow的代理收集后则直接返回结果,因为只浅代理,只创建一层代理,如果不是shallow并且子属性是对象的话就动态创建当前类型的子对象代理。

  vue的代理深创建子对象proxy时是使用时才创建,比如const ra = readonly({a: {b: 2}})使用这段代码时会创建外层{a: ...}代理,{b: 2}这层并不会马上创建,而是当你使用ra.b时会在get拦截器中创建。

  vue中有一些保留属性是不会收集和创建子代理的例如__proto__(原型),__v_isRef(是否是ref),__isVue(是否是vue)

  除此之外还有一些系统自带的Symbol也不会处理,比如Symbol.toStringTagSymbol.iterator,为什么这里不需要收集呢,因为这里在实现时如果用到代理对象也会被收集到,而比如下面这段代码中,在具体的Symbol执行时也是在effect函数内,也会被收集到,而数组内也是通过下标来get也会收集到具体的index

const pig = {
  name: '猪',
  get [Symbol.toStringTag]() {
    return this.name
  }
}
effect(() => {
  // 经过[Symbol.toStringTag]函数收集到name属性依赖
  pig.toString()
})
pig.name = '佩奇'

  当获取到的数据是ref数据时如果不是数组子项的话代理还会自动解包。不知道为啥设计成数组子项不自动解包,我猜测是因为防止reactive([1, ref(2)])使用时都是数字造成混乱。

Array 改造器

  当代理对象是Array时还需要还需要对一些的api做特殊处理,因为数组通过一些api获取会引发混乱,当用户使用indexOflastIndexOfincludes时因为底层是通过this[index]来获取的,this就是proxy,在vue中如果当前子项是对象会转化为代理,就会造成通过原始对象找不到在数组中的位置,比如没处理的话下方代码会出现这种情况

const target = {};
const observed = reactive([target]);
// -1, 因为比对的是 reactive(target) === target
observed.indexOf(target);

  在通过pushpopunshiftsplice写入和删除时底层会获取当前数组的length属性,如果在effect中使用时自然也会收集这个属性的依赖,当使用这些api是也会更改length,这时容易造成死循环,所以这些方法也需要特殊处理,比如没处理的话下方代码会出现这种情况

const observed: number[] = reactive([]);
// e1: 采集到length依赖,并更改length
effect(() => {
  observed.push(1);
});
// e2: 采集到length依赖,并更改length,e1依赖length收到触发重新执行
effect(() => {
  observed.push(2);
});
// [ 1, 2, 1 ]
console.log(observed);

  下面我们看看Array改造器源码

// 数组的函数改造器
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()

function createArrayInstrumentations() {
  const instrumentations: Record<string, Function> = {}
  // 需要对比获取源数据来比对的api
  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      // 获取源数组
      const arr = toRaw(this) as any
      // 因为不通过代理获取,所以需要手动track收集每个子项
      for (let i = 0, l = this.length; i < l; i++) {
        track(arr, TrackOpTypes.GET, i + '')
      }
      // 获取结果,如果获取不到结果,则将传入(可能传入代理)转换为源数据再次获取
      const res = arr[key](...args)
      if (res === -1 || res === false) {
        return arr[key](...args.map(toRaw))
      } else {
        return res
      }
    }
  })
  // 为了某些情况会无限循环,对arry的length会改变的阻止收集
  // 属于对数组的更改,但是写在effect时互相引用时会容易造成无限循环
  ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      // 暂停收集
      pauseTracking()
      // 通过源数组更改,不走代理
      const res = (toRaw(this) as any)[key].apply(this, args)
      // 恢复收集
      resetTracking()
      return res
    }
  })
  return instrumentations
}

  为了确保indexOfincludeslastIndexOf等 api 能够获取到正确结果会将代理转化为Target对象,api传入对象先查找,如果查找不到再讲传入数据转化为raw查找比对。push, pop, shift, unshift, splice等 api 会在调用时暂停收集,因为他们本身也是更改数据,不用收集。

  track采用栈的方式管理,恢复是恢复上一次的状态,我们看一下源码

// 当前是否开启跟踪
let shouldTrack = true;
// 跟踪栈
const trackStack: boolean[] = [];

// 暂停跟踪
export function pauseTracking() {
  trackStack.push(shouldTrack);
  shouldTrack = false;
}

// 启用跟踪
export function enableTracking() {
  trackStack.push(shouldTrack);
  shouldTrack = true;
}

// 恢复上一次跟踪,如果没有上一次默认为true
export function resetTracking() {
  const last = trackStack.pop();
  shouldTrack = last === undefined ? true : last;
}

  到这里get拦截器就看完了,但是没有看到与effect关联的部分,都是统一track出去的,可以猜测到区分是否在effectget应该是在effect这个函数中实现的,这个我们后面再讲。

set 拦截器和 delete 拦截器

  在思考实现时我们说到set拦截器主要职责是重新触发effect的执行,在vue中是使用trigger这个函数来实现的,delete拦截器也是属于修改同样的职责,接下来我们看看createSetterdeleteProperty函数源码:

// 创建set 拦截器
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // 获取旧数据
    let oldValue = (target as any)[key]
    // 如果当前不是shallow并且不是只读的
    if (!shallow && !isReadonly(value)) {
      // 获取target属性,如reacitve(ref(1))获取回来就是ref(1)
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      // 并且target不是数组并且旧数据是ref,新数据不是则直接赋值value
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    }

    // 查看当前更新key是否存在
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // 如果是通过原型链触发的修改,则不trigger
    if (target === toRaw(receiver)) {
      // 查看是否存在,存在是否修改,再触发trigger
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

// del 代理器
function deleteProperty(target: object, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

  setdelete拦截器相对于get拦截器就简单很多了,setdelete拦截器是可读写代理进入的,主要是修改,新增还是删除属性,当trigger时把具体细节传出。set还会自动解包ref并赋值。

  set时如果是shallow或者readonly类型时不做任何处理,直接保持原样设置。如果Target不是数组,旧值是ref新值不是,则直接更新旧值refvalue,这里为什么不用trigger直接返回,这是是因为ref里面已经trigger了,关于ref的实现我们下一章再讲。

  注意:如果是数组,不管是不是下标属性,都是直接赋值,不会解包。上面get拦截器解包逻辑不太一样,get拦截器中如果是数组,非下标会解包,而set拦截器不管是不是下标都不会解包,也就是说会出现下面这种情况,使用感觉不出来,但是还是要注意一下。

const observed1 = reactive([1, 2, 3] as any)
observed1._a = ref(2)
// observed._a 时会自动解包 返回2 实际上存储的是 ref(2)
observed1._a = 4
// observed._a 返回4 实际上存储的是4

const observed2 = reactive({} as any)
observed2._a = ref(2)
// observed._a 时会自动解包 返回2 实际上存储的是 ref(2)
observed2._a = 4
// observed._a 时会自动解包 返回4 实际上存储的是 ref(4)

  上面有段target === toRaw(receiver)来区分是否是原型上触发的判断,它为什么能区分出是否是原型呢,因为在set拦截器中第四个参数是指向当前操作者的this的,假如代理是附加在某个对象的原型上,那么指向的就是这个对象而不是代理。

has 拦截器和 ownKeys 拦截器

  has拦截器和ownKeys拦截器主要职责也是track,告诉vue依赖了哪些属性,与get拦截器一样内置的Symbol不做收集。当Target是数组时触发的ownKeys拦截器会将当前收集的 key 当做是length属性变化,这是因为用户可能在effect中使用array.lengthfor (const atom of array),可以统一使用length一个属性来标识,这样当length改变时统一通知就可以了,如果for (const atom of array)使用ITERATE_KEY会触发两次,一次时length修改,一次时ITERATE_KEY修改。

// has 拦截器
function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key);
  // 查看是否是内置的symbol如果是则不track
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    track(target, TrackOpTypes.HAS, key);
  }
  return result;
}

// getOwnPropertyNames getOwnPropertySymbols keys 拦截器
function ownKeys(target: object): (string | symbol)[] {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? "length" : ITERATE_KEY);
  return Reflect.ownKeys(target);
}

集合拦截器

  在上面我们讲到因为Proxy代理具有内部插槽的内建对象时和集合获取和存储数据时的限制,所以必须将拦截器与普通对象区分出来。如果是你你会怎么实现呢?

  其实在上面代理Array时已经有例子了,既然只能拦截到方法和属性,那么我们就通过改写集合Proxy上的方法和属性来实现就行了,下面我们列出集合上的所有的方法和属性,并标注我们需要做的事情。

方法和属性MapWeakMapSetWeakSet操作
getYYNNtrack,类型:GETkey:用户传入
sizeYNYNtrack,类型:ITERATEkeyITERATE_KEY
hasYYYYtrack,类型:HASkey:用户传入
addNNYYtrigger,类型: ADDkey:用户传入setkey就是value
setYYNNtrigger,类型: SETADDkey:用户传入
deleteYYYYtrigger,类型: DELETEkey:用户传入
clearYNYNtrigger,类型: CLEARkeyundefined
forEachYNYNtrack,类型:ITERATEkeyITERATE_KEY
keysYNYNtrack,类型:ITERATEkeyMAP_KEY_ITERATE_KEY
valuesYNYNtrack,类型:ITERATEkeyITERATE_KEY
entriesYNYNtrack,类型:ITERATEkeyITERATE_KEY
Symbol.iteratorYNYNtrack,类型:ITERATEkeyITERATE_KEY

  在上面中我们看到常用拦截器有四种分别是mutableCollectionHandlersreadonlyCollectionHandlersshallowCollectionHandlersshallowReadonlyCollectionHandlers分别对应reactivereadonlyshallowReactiveshallowReadonly集合代理类型接下来我们看看源码实现:

// 创建集合get拦截器,附加flags,和改造api
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  // 获取各个版本的修改器
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
    ? readonlyInstrumentations
    : mutableInstrumentations;

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly;
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly;
    } else if (key === ReactiveFlags.RAW) {
      return target;
    }

    // 使用拦截器返回用户使用函数
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    );
  };
}

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, false),
};

export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, true),
};

export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(true, false),
};

export const shallowReadonlyCollectionHandlers: ProxyHandler<CollectionTypes> =
  {
    get: /*#__PURE__*/ createInstrumentationGetter(true, true),
  };

  和我们猜想一样,vue只代理了集合的get,因为无法拦截set,所以通过修改方法和属性可以将我们需要做的操作附加上去。通过闭包将是否只读,是否是shallow缓存,并生成相对应的修改器。

  跟常用拦截器一样也会为集合Proxy附加上各个flags属性。当获取某个属性时,如果这个属性是在修改器对象上,并且也在当前集合上,那么就会从修改器中获取这个方法或者属性返回。为什么还要获取是否在是否在集合上呢,因为修改器是通过Proxy类型分类的,而不是通过集合类型分类的,那么修改器中可能会同时存在addset,如果不判断一遍是否存在集合上那么map.add也会调用到修改器中的add方法。接下来我们看看创建修改器的具体实现。

// 只读版本修改方法,只弹出警告
function createReadonlyMethod(type: TriggerOpTypes): Function {
  return function (this: CollectionTypes, ...args: unknown[]) {
    if (__DEV__) {
      const key = args[0] ? `on key "${args[0]}" ` : ``
      console.warn(
        `${capitalize(type)} operation ${key}failed: target is readonly.`,
        toRaw(this)
      )
    }
    return type === TriggerOpTypes.DELETE ? false : this
  }
}

// 创建各个版本的拦截处理器
function createInstrumentations() {
  const mutableInstrumentations: Record<string, Function> = {
    get(this: MapTypes, key: unknown) {
      return get(this, key)
    },
    get size() {
      return size(this as unknown as IterableCollections)
    },
    has,
    add,
    set,
    delete: deleteEntry,
    clear,
    forEach: createForEach(false, false)
  }

  const shallowInstrumentations: Record<string, Function> = {
    get(this: MapTypes, key: unknown) {
      return get(this, key, false, true)
    },
    get size() {
      return size(this as unknown as IterableCollections)
    },
    has,
    add,
    set,
    delete: deleteEntry,
    clear,
    forEach: createForEach(false, true)
  }

  const readonlyInstrumentations: Record<string, Function> = {
    get(this: MapTypes, key: unknown) {
      return get(this, key, true)
    },
    get size() {
      return size(this as unknown as IterableCollections, true)
    },
    has(this: MapTypes, key: unknown) {
      return has.call(this, key, true)
    },
    add: createReadonlyMethod(TriggerOpTypes.ADD),
    set: createReadonlyMethod(TriggerOpTypes.SET),
    delete: createReadonlyMethod(TriggerOpTypes.DELETE),
    clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
    forEach: createForEach(true, false)
  }

  const shallowReadonlyInstrumentations: Record<string, Function> = {
    get(this: MapTypes, key: unknown) {
      return get(this, key, true, true)
    },
    get size() {
      return size(this as unknown as IterableCollections, true)
    },
    has(this: MapTypes, key: unknown) {
      return has.call(this, key, true)
    },
    add: createReadonlyMethod(TriggerOpTypes.ADD),
    set: createReadonlyMethod(TriggerOpTypes.SET),
    delete: createReadonlyMethod(TriggerOpTypes.DELETE),
    clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
    forEach: createForEach(true, true)
  }

  // 各个迭代器拦截器
  const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
  iteratorMethods.forEach(method => {
    mutableInstrumentations[method as string] = createIterableMethod(
      method,
      false,
      false
    )
    readonlyInstrumentations[method as string] = createIterableMethod(
      method,
      true,
      false
    )
    shallowInstrumentations[method as string] = createIterableMethod(
      method,
      false,
      true
    )
    shallowReadonlyInstrumentations[method as string] = createIterableMethod(
      method,
      true,
      true
    )
  })

  return [
    mutableInstrumentations,
    readonlyInstrumentations,
    shallowInstrumentations,
    shallowReadonlyInstrumentations
  ]
}

const [
  mutableInstrumentations,
  readonlyInstrumentations,
  shallowInstrumentations,
  shallowReadonlyInstrumentations
] = /* #__PURE__*/ createInstrumentations()

  可以看到构建时大部分方法都是公用的,只是传入参数不同为了区分readonlyshallow。只读版本的Proxy做增删改时都是弹出警告。所以真实的实现只有gethassizesetadddeleteclearcreateForEachcreateIterableMethod等函数。

  keys, values, entries, Symbol.iterator,都使用createIterableMethod来构建的,为了区分还会将当前的method传入。

  其中集合的size是属性,为了能够附加操作还将它改造成getter函数。

get 修改器、size 修改器和 has 修改器

// 辅助方法,转化为shallow 什么都不做
const toShallow = <T extends unknown>(value: T): T => value

// get拦截器
function get(
  target: MapTypes,
  key: unknown,
  isReadonly = false,
  isShallow = false
) {
  // 如果出现readonly(reactive(Map)) 的情况,在readonly代理中获取到reactive(Map),
  // 确保get时也要经过reactive代理
  target = (target as any)[ReactiveFlags.RAW]
  // 获取 target
  const rawTarget = toRaw(target)
  // 获取 key target
  const rawKey = toRaw(key)
  // 如果key是响应式的
  if (key !== rawKey) {
    // 再track收集一遍响应式key
    // 那么用户不管 trigger的是rawKey 还是 key都会触发得到
    !isReadonly && track(rawTarget, TrackOpTypes.GET, key)
  }
  // 收集访问了哪个key
  !isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)

  // 获取集合原型上的has方法
  const { has } = getProto(rawTarget)
  // 获取返回值处理函数,根据当前代理类型,将返回值转化为相对应的代理类型
  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive

  // 确保 包装后的key 和没包装的key都能访问得到
  if (has.call(rawTarget, key)) {
    return wrap(target.get(key))
  } else if (has.call(rawTarget, rawKey)) {
    return wrap(target.get(rawKey))
  } else if (target !== rawTarget) {
    // 如果target !== rawTarget,那么就是如果出现readonly(reactive(Map))的情况,
    // 确保也要经过reactive代理处理
    target.get(key)
  }
}

// has 拦截器 是否shallow处理方式都一样
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
  // 获取代理前数据
  const target = (this as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  // 如果key是响应式的都收集一遍
  if (key !== rawKey) {
    !isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
  }
  !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)

  // 如果key是proxy, 那么先获取has(keyProxy),再获取has(key)确保获取结果正确
  return key === rawKey
    ? target.has(key)
    : target.has(key) || target.has(rawKey)
}

// size 拦截器 是否shallow处理方式都一样
function size(target: IterableCollections, isReadonly = false) {
  // 获取封装对象
  target = (target as any)[ReactiveFlags.RAW]
  // 收集获取迭代
  !isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.get(target, 'size', target)
}


  和常用拦截器附加逻辑差不多,和将获取的值转化为当前proxy类型的proxytrack依赖,不过有细微的区别。

  看到(target as any)[ReactiveFlags.RAW]会获取当前代理前的数据,这是为了防止readonly(reactive(Map))类型的代理获取的值返回的是readonly类型的代理,所以获取readonly前的数据也就是reactive(Map)进行get这样获取也会经过reactive代理修改器,获取回来的就是toReadonly(toReactive(Raw))。为什么常用处理器不用呢,因为它是通过代理拦截器的第一个参数直接获取的,就是代理前的数据。

  还有当MapWeakMap获取数据是,会检测key是否是Proxy,如果是的话,会track两次,这是因为MapWeakMapkey可以是对象,用户可能将Proxy当做key,当然在set的时候会将key转化为原始数据存储,但是在初始化的时候不会做检测,所以为了确保能正确的触发,两次收集时必要的,否则当存储是proxykey的话将无法触发。例如:

const map = new Map();
const key = reactive({});
map.set(key, 1);
const rMap = reactive(map);

effect(() => {
  map.get(key);
});

// 如果没有收集到 keyProxy的话将无法触发
map.set(key, 2);

set 修改器、delete 修改器和 clear 修改器

// 检查key是否是响应式的
function checkIdentityKeys(
  target: CollectionTypes,
  has: (key: unknown) => boolean,
  key: unknown
) {
  const rawKey = toRaw(key);
  if (rawKey !== key && has.call(target, rawKey)) {
    const type = toRawType(target);
    console.warn(
      `Reactive ${type} contains both the raw and reactive ` +
        `versions of the same object${type === `Map` ? ` as keys` : ``}, ` +
        `which can lead to inconsistencies. ` +
        `Avoid differentiating between the raw and reactive versions ` +
        `of an object and only use the reactive version if possible.`
    );
  }
}

// Map set处理器
function set(this: MapTypes, key: unknown, value: unknown) {
  // 存origin value
  value = toRaw(value);
  // 获取origin target
  const target = toRaw(this);
  const { has, get } = getProto(target);

  // 查看当前key是否存在
  let hadKey = has.call(target, key);
  // 如果不存在则获取 origin
  if (!hadKey) {
    key = toRaw(key);
    hadKey = has.call(target, key);
  } else if (__DEV__) {
    // 检查当前是否包含原始版本 和响应版本在target中
    checkIdentityKeys(target, has, key);
  }

  // 获取旧的value
  const oldValue = get.call(target, key);
  // 设置新值
  target.set(key, value);
  if (!hadKey) {
    trigger(target, TriggerOpTypes.ADD, key, value);
  } else if (hasChanged(value, oldValue)) {
    trigger(target, TriggerOpTypes.SET, key, value, oldValue);
  }
  return this;
}

// Set add拦截器
function add(this: SetTypes, value: unknown) {
  // 存origin value
  value = toRaw(value);
  // 获取origin target
  const target = toRaw(this);
  const proto = getProto(target);
  // 查看是否存在要添加的value
  const hadKey = proto.has.call(target, value);
  // 不存在添加,并且trigger
  if (!hadKey) {
    target.add(value);
    trigger(target, TriggerOpTypes.ADD, value, value);
  }
  return this;
}

// clear拦截器
function clear(this: IterableCollections) {
  const target = toRaw(this);
  // 获取是否存在数据
  const hadItems = target.size !== 0;
  // 构建一个map set作为旧数据
  const oldTarget = __DEV__
    ? isMap(target)
      ? new Map(target)
      : new Set(target)
    : undefined;

  const result = target.clear();
  if (hadItems) {
    trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget);
  }
  return result;
}

  这些修改器也和我们猜想的差不多,主要职责是trigger。并且会确保setadd进去的值是Target而不是Proxy。其中MapWeakMapset修改器还会检测当前key是否存储了两份版本即是proxy和原始版本的key,如果有的话则弹出警告。SetWeakSetadd修改器会检测当前是否存在,如果存在则不在触发因为Set内不是会有重复数据的。

createForEach 修改器和 createIterableMethod 修改器

// forEach拦截器
function createForEach(isReadonly: boolean, isShallow: boolean) {
  return function forEach(
    this: IterableCollections,
    callback: Function,
    thisArg?: unknown
  ) {
    const observed = this as any
    const target = observed[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    // 转化器
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    // track当前
    !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
    // origin foreach
    return target.forEach((value: unknown, key: unknown) => {
      // 确保用户拿到的值是响应式的
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    })
  }
}

// 创建迭代器拦截器  keys values Symbol.interator使用
function createIterableMethod(
  method: string | symbol,
  isReadonly: boolean,
  isShallow: boolean
) {
  // 返回迭代器
  return function (
    this: IterableCollections,
    ...args: unknown[]
  ): Iterable & Iterator {
    // 获取封装对象
    const target = (this as any)[ReactiveFlags.RAW]
    // 源对象 
    const rawTarget = toRaw(target)
    // 是否是map
    const targetIsMap = isMap(rawTarget)
    // 是否需要返回一对[key, val]
    const isPair =
      method === 'entries' || (method === Symbol.iterator && targetIsMap)
    // 是否只需要返回key
    const isKeyOnly = method === 'keys' && targetIsMap
    // 获取封装对象迭代器
    const innerIterator = target[method](...args)
    // 转化函数
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    // track当前对象
    !isReadonly &&
      track(
        rawTarget,
        TrackOpTypes.ITERATE,
        isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
      )
      
    return {
      // 迭代器直接使用
      next() {
        // 直接使用封装对象迭代器,再转化为响应式版本
        const { value, done } = innerIterator.next()
        return done
          ? { value, done }
          : {
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
              done
            }
      },
      // 给 keys values entries 方法调用时创建迭代器
      [Symbol.iterator]() {
        return this
      }
    }
  }
}

  我们先看看forEach修改器的实现,这里会将获取的每个keyvalue都转化为相对应的代理,确保用户通过Proxy获取的都是Proxy,也相对比较简单。

  在看迭代器修改器前我们先回忆一下迭代器对象的实现方式,首先迭代器对象上必须返回带有Symbol.iterator方法,这个方法必须有next方法,next方法需要放回带有valuedone属性的对象,当donetrue时表示完成迭代到达边界,下面我们实现一个简单的迭代器对象:

let i = 0
const test = {
  [Symbol.iterator]() {
    return {
      next() {
        if (i < 5) {
          return {
            value: i++,
            done: false
          }
        } else {
          return {
            done: true
          }
        }
      }
    }
  }
}
// [0,1,2,3,4]
console.log([...test])

  接下来我们再看迭代器修改器的实现,keys, values, entries, 等方法都有一个共同的特征就是返回的是迭代器对象,所以当执行的时候要确保能够返回{[Symbol.iterator](): {next: ...}},当访问Symbol.iterator时就直接返回{next: ...},可以看到这段代码写的很精妙,为了确保两者都能兼容直接在Symbol.iterator的实现中返回this。为了方便理解我将不必要代码剔除掉,如下:

const keyInterator = map.keys()
/**
 * keyInterator相当于
 *  {
 *    [Symbol.iterator]() {
 *      return {
 *        next () {
 *          ...
 *        }
 *      }
 *    }
 *  }
 **/

  vue通过特定的method和是否是Map来判断当前需要返回的是键值对还是单值,同时确认好key是使用MAP_KEY_ITERATE_KEY还是ITERATE_KEY,会用使用集合自身的迭代器实现获取结果,然后转化为与当前Proxy类型一致的值返回给用户。

  到这里我们reactive中具体的实现就已经看完了,这里我们总结一下重要的知识点。

小结

  • 创建代理对象的Target必须为对象,不能为基础对象
  • reactive类型可以转化为readonly但是不能逆转
  • 同一个对象不会创建两次,会在map中查看当前对象是否已经创建,如果创建了直接返回缓存中的
  • Proxy对象不能创建原型被冻结的和被mark的对象
  • tracktrigger需要传入具体细节
  • Proxy深入创建时是延迟创建的,在get获取子对象时才会创建子Proxy
  • readonlyget时不会track属性,因为不可变
  • 系统自带的Symbol和保留属性__proto____v_isRef__isVue不会加入track和创建Proxy
  • Proxyget到的是ref并且Target不是数组或者是数组但是key不是数组下标时,会自动解包
  • 当代理Array时,会修改代理上的indexOflastIndexOfincludespushpopunshiftsplice方法
  • 使用/*#__PURE__*/告诉打包器是纯变量,没使用时可删减减少包体积
  • gethasownKeys主要职责是track
  • setdelete主要职责是trigger
  • 深入创建proxy是在get拦截器进行的
  • 因为局限性所以集合类代理是通过修改api来实现的
  • Proxy获取出来的对象始终是与当前Proxy类型相同的Proxy
  • Proxy存储数据时始终是存储的原始对象

下一章:vue3-effect源码解析