最近一阶段在学习Vue3,Vue3中用 reactiveref 等办法将数据转化为响应式数据,在获取时应用 trackeffect 中收集依赖,在值扭转时,应用 trigger 触发依赖,执行对应的监听函数,这次就先来看一下 reactive 的源码。

reactive的源码在官网源码的packages/reactivity/src/reactive.ts文件中,源码中提供了四个Api来创立reactive类对象:

  • reactive:创立可深刻响应的可读写对象
  • readonly:创立可深刻响应的只读对象
  • shallowReactive:创立只有第一层响应的浅可读写对象(其余层,值扭转视图不更新)
  • shallowReadonly:创立只有一层响应的浅只读对象

它们都是调用createReactiveObject办法来创立响应式对象,区别在于传入不同的参数,本文只讲reactive,其余几个大同小异:

export function reactive(target: object) {  // 如果是只读的话间接返回  if (isReadonly(target)) {    return target  }  return createReactiveObject(    // 指标对象    target,    // 标识是否是只读    false,    // 罕用类型拦截器    mutableHandlers,    // 汇合类型拦截器    mutableCollectionHandlers,    // 储了每个对象与代理的map关系    reactiveMap  )}export const reactiveMap = new WeakMap<Target, any>()

createReactiveObject代码如下:

function createReactiveObject(  target: Target,  isReadonly: boolean,  baseHandlers: ProxyHandler<any>,  collectionHandlers: ProxyHandler<any>,  proxyMap: WeakMap<Target, any>) {  // 如果代理的数据不是对象,则间接返回原对象  if (!isObject(target)) {    return target  }  // 如果传入的曾经是代理了 并且 不是readonly 转换 reactive的间接返回  if (    target[ReactiveFlags.RAW] &&    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])  ) {    return target  }  // 查看以后代理对象之前是不是创立过以后代理,如果创立过间接返回之前缓存的代理对象  // proxyMap 是一个全局的缓存WeakMap  const existingProxy = proxyMap.get(target)  if (existingProxy) {    return existingProxy  }  // 如果以后对象无奈创立代理,则间接返回源对象  const targetType = getTargetType(target)  if (targetType === TargetType.INVALID) {    return target  }  //  依据targetType 抉择汇合拦截器还是根底拦截器  const proxy = new Proxy(    target,    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers  )  // 向全局缓存Map里存储  proxyMap.set(target, proxy)  return proxy}

其中有个办法是 getTargetType,用来获取传入target的类型:

function getTargetType(value: Target) {  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)    ? TargetType.INVALID    : targetTypeMap(toRawType(value))}export const enum ReactiveFlags {  SKIP = '__v_skip',              // 标记阻止成为代理对象  IS_REACTIVE = '__v_isReactive', // 标记一个响应式对象  IS_READONLY = '__v_isReadonly', // 标记一个只读对象  IS_SHALLOW = '__v_isShallow',   // 标记只有一层响应的浅可读写对象  RAW = '__v_raw'                 // 标记获取原始值}const enum TargetType {  // 有效的 比方根底数据类型  INVALID = 0,  // 常见的 比方object Array  COMMON = 1,  // 汇合类型比方 map set  COLLECTION = 2}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  }}

当target被标记为 ReactiveFlags.SKIP 或是 不可拓展的,则会返回 TargetType.INVALID,无奈创立代理,因为Vue须要对Target代理附加很多货色,如果是不可拓展的则会附加失败;或是用户被动调用 markRaw 等办法将数据标记为非响应式数据,那么也无奈创立代理。

export function markRaw<T extends object>(value: T): T {  def(value, ReactiveFlags.SKIP, true)  return value}

看完了入口函数,接下来就是创立Proxy对象的过程了,Vue3会依据getTargetType返回的数据类型来抉择是应用collectionHandlers汇合拦截器还是baseHandlers罕用拦截器,起因上面讲到汇合拦截器的时候再说。

罕用拦截器baseHandlers:
  1. get 拦截器:

    function createGetter(isReadonly = false, shallow = false) {  return function get(target: Target, key: string | symbol, receiver: object) { if (key === ReactiveFlags.IS_REACTIVE) { // 获取以后是否是reactive   return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { // 获取以后是否是readonly   return isReadonly } else if (key === ReactiveFlags.IS_SHALLOW) { // 获取以后是否是shallow   return shallow } else if (   // 如果获取源对象,在全局缓存WeakMap中获取是否有被创立过,如果创立过间接返回被代理对象   key === ReactiveFlags.RAW &&   receiver ===     (isReadonly       ? shallow         ? shallowReadonlyMap         : readonlyMap       : shallow       ? shallowReactiveMap       : reactiveMap     ).get(target) ) {   return target } // 是否是数组 const targetIsArray = isArray(target) // arrayInstrumentations相当于一个革新器,外面定义了数组须要革新的办法,进行一些依赖收集等操作 // 如果是数组,并且拜访的办法在革新器中,则应用革新器获取 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) : isNonTrackableKeys(key)) {   return res } // 如果不是只读则收集依赖,Vue3中用track收集依赖 if (!isReadonly) {   track(target, TrackOpTypes.GET, key) } // shallow只有表层响应式,不须要上面去深度创立响应了 if (shallow) {   return res } // 如果获取的值是ref类型 if (isRef(res)) {   // 如果是数组 并且 是int类型的 key,则返回,否则返回.value属性   return targetIsArray && isIntegerKey(key) ? res : res.value } if (isObject(res)) {   // *获取时才创立绝对应类型的代理,将拜访值也转化为reactive,不是一开始就将所有子数据转换   return isReadonly ? readonly(res) : reactive(res) } return res  }}

    留神点是当代理类型是 readonly 时,不会收集依赖。
    Vue3对于深层次的对象是应用时才创立的,还有如果后果是ref类型,则须要判断是否要获取它的.value类型,举个:

    const Name = ref('张三')const Array = ref([1])const data = reactive({  name: Name,  array: Array})console.log(Name)          // RefImpl类型console.log(data.name)     // 张三console.log(data.array[0]) // 1

    Vue3中应用 arrayInstrumentations对数组的局部办法做了解决,为什么要这么做呢? 对于 pushpopshiftunshiftsplice 这些办法,写入和删除时底层会获取以后数组的length属性,如果咱们在effect中应用的话,会收集length属性的依赖,当应用这些api是也会更改length,就会造成死循环:

     let arr = [] let proxy = new Proxy(arr, {get: function(target, key, receiver) {  console.log(key)  return Reflect.get(target, key, receiver)} }) proxy.push(1) /* 打印 */ // push // length
    // 当把这个代码正文掉时// if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {//     return Reflect.get(arrayInstrumentations, key, receiver);// }const arr = reactive([])watchEffect(() => { arr.push(1)})watchEffect(() => { arr.push(2)     // 下面的effect里收集了对length的依赖,push又扭转了length,所以下面的又会触发,以此类推,死循环})// [1,2,1,2 ...] 死循环console.log(arr)

    对于 includesindexOflastIndexOf,外部会去获取每一个的值,下面讲到如果获取进去的后果是Obejct,会主动转换为reactive对象:

    let target = {name: '张三'}const arr = reactive([target])console.log(arr.indexOf(target)) // -1

    因为实际上是 reactive(target)target 在比照,当然查不到。

  2. set 拦截器

    function createSetter(shallow = false) { return function set(target, key, value, receiver) {     // 获取旧数据     let oldValue = target[key];     if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {         return false;     }     // 如果以后不是shallow并且不是只读的     if (!shallow && !isReadonly(value)) {         if (!isShallow(value)) {             // 如果新value自身是响应对象,就把他变成一般对象             // 在get中讲到过如果取到的值是对象,才转换为响应式             // vue3在代理的时候,只代理第一层,在应用到的时候才会代理第二层             value = toRaw(value);             oldValue = toRaw(oldValue);         }         // 如果旧的值是ref对象,新值不是,则间接赋值给ref对象的value属性         if (!isArray(target) && isRef(oldValue) && !isRef(value)) {             // 这里不触发trigger是因为,ref对象在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);     // 这个判断次要是为了解决代理对象的原型也是代理对象的状况     if (target === toRaw(receiver)) {         if (!hadKey) {             // key不存在就 触发add类型的依赖更新             trigger(target, "add" /* ADD */, key, value);         }         else if (hasChanged(value, oldValue)) {             // key存在就触发set类型依赖更新             trigger(target, "set" /* SET */, key, value, oldValue);         }     }     return result; };}

    set中还有一个要留神的中央就是 target === toRaw(receiver),这次要是为了解决代理对象的原型也是代理对象的状况:

    const child = reactive({})let parentName = ''const parent = reactive({  set name(value) {     parentName = value  },  get name() {     return parentName  }})Object.setPrototypeOf(child, parent)child.name = '张三'console.log(toRaw(child)) // {name: 张三}console.log(parentName) // 张三

    当这种时候,如果不加上这个判断,因为子代理没有name这个属性,会触发原型父代理的set,加上这个判断防止父代理也触发更新。

汇合拦截器collectionHandlers:

汇合类型的数据比拟非凡,其相干实例办法Proxy没有提供相干的捕捉器,然而因为办法调用属于属性获取操作,所以都能够通过捕捉get操作来实现,所以Vue3也只定义了get拦挡:

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {  get: /*#__PURE__*/ createInstrumentationGetter(false, false)}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    )  }}

之前的文章《代理具备外部插槽的内建对象》中说过Proxy代理具备外部插槽的内建对象,拜访Proxy上的属性会产生谬误。Vue3中是如何解决的呢?

Vue3中新创建了一个和汇合对象具备雷同属性和办法的一般对象,在汇合对象 get 操作时将 target 对象换成新创建的一般对象。这样,当调用 get 操作时 Reflect 反射到这个新对象上,当调用 set 办法时就间接调用新对象上能够触发响应的办法,这样拜访的就不是Proxy上的办法,是这个新对象上的办法:

function createInstrumentations() {  const mutableInstrumentations: Record<string, Function> = {    get(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 iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]  iteratorMethods.forEach(method => {    mutableInstrumentations[method as string] = createIterableMethod(      method,      false,      false    )  })  return [    mutableInstrumentations  ]}

接下来看一看几个具体的拦截器:

  1. get 拦截器:

    function get(  target: MapTypes,  key: unknown,  isReadonly = false,  isShallow = false) {  // 如果呈现readonly(reactive())这种嵌套的状况,在readonly代理中获取到reactive()  // 确保get时也要通过reactive代理  target = (target as any)[ReactiveFlags.RAW]  const rawTarget = toRaw(target)  const rawKey = toRaw(key)  // 确保 包装后的key 和 没包装的key 都能拜访失去  if (!isReadonly) {     if (key !== rawKey) {       track(rawTarget, TrackOpTypes.GET, key)     }     track(rawTarget, TrackOpTypes.GET, rawKey)  }  const { has } = getProto(rawTarget)  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive  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.get(key)  }}

    汇合拦截器里把 keyrawKey 都做了解决,保障都能取到数据:

    let child = {  name: 'child'}const childProxy = reactive(child)const map = reactive(new Map())map.set(childProxy, 1234)console.log(map.get(child)) // 1234console.log(map.get(childProxy)) // 1234
  2. set 拦截器:

    // 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;}
  3. has 拦截器:

    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 那么先拜访 proxyKey 在拜访 原始key 获取后果  return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey)}
  4. 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 !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY) // 劫持传递进来的callback,让传入callback的数据转换成响应式数据 return target.forEach((value: unknown, key: unknown) => {   // 确保拿到的值是响应式的   return callback.call(thisArg, wrap(value), wrap(key), observed) })  }}

结尾

我是周小羊,一个前端萌新,写文章是为了记录本人日常工作遇到的问题和学习的内容,晋升本人,如果您感觉本文对你有用的话,麻烦点个赞激励一下哟~