关于vue.js:Vue3响应式源码分析-reactive篇

3次阅读

共计 10263 个字符,预计需要花费 26 分钟才能阅读完成。

最近一阶段在学习 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)) // 1234
    console.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)
     })
      }
    }

结尾

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

正文完
 0