乐趣区

关于前端:vue3源码七reactiveObject的响应式实现

【vue3 源码】七、reactive——Object 的响应式实现

参考代码版本:vue 3.2.37

官网文档:https://vuejs.org/

reactive返回一个对象的响应式代理。

应用

const obj = {
  count: 1,
  flag: true,
  obj: {str: ''}
}

const reactiveObj = reactive(obj)

源码解析

reactive

export function reactive(target: object) {
  // 如果 target 是个只读 proxy,间接 return
  if (isReadonly(target)) {return target}
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

reactive首先判断 target 是不是只读的 proxy,如果是的话,间接返回target;否则调用一个createReactiveObject 办法。

createReactiveObject

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {if (!isObject(target)) {if (__DEV__) {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[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {return target}
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {return existingProxy}
  // only a whitelist of value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {return target}
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

createReactiveObject接管五个参数:target被代理的对象,isReadonly是不是只读的,baseHandlersproxy 的捕捉器,collectionHandlers针对汇合的 proxy 捕捉器,proxyMap一个用于缓存 proxy 的 WeakMap 对象

如果 target 不是Object,则进行提醒,并返回target

if (!isObject(target)) {if (__DEV__) {console.warn(`value cannot be made reactive: ${String(target)}`)
  }
  return target
}

isObject

export const isObject = (val: unknown): val is Record<any, any> =>
  val !== null && typeof val === 'object'

如果 target 曾经是个 proxy,间接返回targetreactive(readonly(obj)) 是个例外。

if (target[ReactiveFlags.RAW] &&
  !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {return target}

而后尝试从 proxyMap 中获取缓存的 proxy 对象,如果存在的话,间接返回 proxyMap 中对应的proxy。否则创立proxy

const existingProxy = proxyMap.get(target)
if (existingProxy) {return existingProxy}

为什么要缓存代理对象?

这里缓存对象的存在意义是,一方面防止对同一个对象进行屡次代理造成的资源节约 另一方面能够保障雷同对象被代理屡次后,代理对象保持一致。例如上面这里例子:

const obj = {}
const objReactive = reactive([obj])
console.log(objReactive.includes(objReactive[0]))

如果没有 proxyMap 这个缓存对象,在 includes 中因为会拜访到数组索引,所以会创立一个 obj 的响应式对象,而在 includes 的参数中,又拜访了顺次 objReactive 的 0 索引,所以又会创立个新的 obj 代理对象。两次创立的代理对象因为地址不统一,造成 objReactive.includes(objReactive[0]) 输入为false。而有了这个缓存对象,当第二次要创立代理对象时,会间接从缓存中获取,这样就保障了雷同对象的代理对象地址一致性的问题。

并不是任何对象都能够被 proxy 所代理。这里会通过 getTargetType 办法来进行判断。

const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {return target}

getTargetType

function getTargetType(value: Target) {return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}

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
  }
}

getTargetType有三种可能的返回后果

  • TargetType.INVALID:代表 target 不能被代理
  • TargetType.COMMON:代表 targetArrayObject
  • TargetType.COLLECTION:代表 targetMapSetWeakMapWeakSet中的一种

target不能被代理的状况有三种:

  1. 显示申明对象不可被代理(通过向对象增加 __v_skip: true 属性)或应用 markRaw 标记的对象
  2. 对象为不可扩大对象:如通过 Object.freezeObject.sealObject.preventExtensions 的对象
  3. 除了 ObjectArrayMapSetWeakMapWeakSet 之外的其余类型的对象,如 DateRegExpPromise

如果 targetType !== TargetType.INVALID,那么则能够进行target 的代理操作了。

const proxy = new Proxy(
  target,
  targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy

new Proxy(target, handler) 时,这里的 handler 有两种:一种是针对 ObjectArraybaseHandlers,一种是针对汇合(SetMapWeakMapWeakSet)的collectionHandlers

为什么这里要分两种 handler 呢?

首先,咱们要晓得在 handler 中咱们要进行依赖的收集和依赖的触发。那么什么状况进行依赖收集和触发依赖呢?当咱们对代理对象执行 读取操作 应该收集对应依赖,而当咱们对代理对象执行 批改操作 时应该触发依赖。

那么什么样的操作被称为读取操作和批改操作呢?

读取操作 批改操作
Object obj.afor...in...key in obj obj.a=1delete obj.a
Array for...of...for...in...arr[index]arr.lengtharr.indexOf/lastIndexOf/includes(item)arr.some/every/forEach arr[0]=1arr.length=0arr.pop/push/unshift/shiftarr.splice/fill/sort
汇合 map/set.sizemap.get(key)map/set.has(key)map/set.forEachmap.keys/values() set.add(value)map.add(key, value)set/map.clear()set/map.delete(key)

对于 ObjectArray、汇合这几种数据类型,如果应用proxy 捕捉它们的读取或批改操作,其实是不一样的。比方捕捉批改操作进行依赖触发时,Object能够间接通过 set(或deleteProperty)捕捉器,而Array 是能够通过 poppush 等办法进行批改数组的,所以须要捕捉它的 get 操作进行独自解决,同样对于汇合来说,也须要通过捕捉 get 办法来解决批改操作。

接下来看下创立 reactive 所须要的两个 handlermutableHandlersObjectArrayhandler)、mutableCollectionHandlers(汇合的handler)。

mutableHandlers

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

对于 ObjectArray,设置了 5 个捕捉器,别离为:getsetdeletePropertyhasownKeys

get 捕捉器

get捕捉器为属性读取操作的捕捉器,它能够捕捉obj.proarray[index]array.indexOf()arr.lengthReflect.get()Object.create(obj).foo(拜访继承者的属性)等操作。

const get = /*#__PURE__*/ createGetter()

function createGetter(isReadonly = false, shallow = false) {return function get(target: Target, key: string | symbol, receiver: object) {if (key === ReactiveFlags.IS_REACTIVE) {return !isReadonly} else if (key === ReactiveFlags.IS_READONLY) {return isReadonly} else if (key === ReactiveFlags.IS_SHALLOW) {return shallow} else if (
      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)

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {return res}

    if (!isReadonly) {track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {return res}

    if (isRef(res)) {const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    if (isObject(res)) {return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

get捕捉器通过一个 createGetter 函数创立。createGetter接管两个参数:isReadonly是否为只读的响应式数据、shallow是否是浅层响应式数据。

get 捕捉器中,会先解决几个非凡的key

  • ReactiveFlags.IS_REACTIVE:是不是reactive
  • ReactiveFlags.IS_READONLY:是不是只读的
  • ReactiveFlags.IS_SHALLOW:是不是浅层响应式
  • ReactiveFlags.RAW:原始值
if (key === ReactiveFlags.IS_REACTIVE) {return !isReadonly} else if (key === ReactiveFlags.IS_READONLY) {return isReadonly} else if (key === ReactiveFlags.IS_SHALLOW) {return shallow} else if (
  key === ReactiveFlags.RAW &&
  receiver ===
  (isReadonly
      ? shallow
        ? shallowReadonlyMap
        : readonlyMap
      : shallow
        ? shallowReactiveMap
        : reactiveMap
  ).get(target)
) {return target}

在获取原始值,有个额定的条件:receiver 全等于 target 的代理对象。为什么要有这个额定条件呢?

这样做是为了防止从原型链上获取不属于本人的原始对象。来看上面一个例子:

const parent = {p:1}

const parentReactive = reactive(parent)
const child = Object.create(parentReactive)

console.log(toRaw(parentReactive) === parent) // true
console.log(toRaw(child) === parent) // false

申明一个变量 parent 并将 parent 应用 proxy 代理,而后应用 Object.create 创立一个对象并将原型指向 parent 的代理对象parentReactive

这时 parentReactive 的原始对象还是parent,这是毫无疑问的。

如果尝试获取 child 的原始对象,因为 child 自身是不存在 ReactiveFlags.RAW 属性的,所以会沿着原型链向上找,找到 parentReactive 时,被 parentReactiveget拦截器捕捉(此时 targetparentreceiverchild),如果没有这条额判断,那么会间接返回target,也就是parent,此时意味着child 的原始对象是parent,这显然是不合理的。恰好就是这个额定条件排除了这种状况。

而后查看 target 是不是数组,如果是数组,须要对一些办法(针对includesindexOflastIndexOfpushpopshiftunshiftsplice)进行非凡解决。

const targetIsArray = isArray(target)

if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)
}

通过判断 key 是不是 arrayInstrumentations 本身蕴含的属性,解决非凡的数组办法。arrayInstrumentations是应用 createArrayInstrumentations 创立的一个对象,该对象属性蕴含要非凡解决的数组办法:includesindexOflastIndexOfpushpopshiftunshiftsplice

为什么要针对这些办法进行非凡解决?

为了弄明确这个问题,咱们申明了一个简略的myReactive,它能够深度创立proxy

const obj = {}

function myReactive(obj) {
  return new Proxy(obj, {get(target, key, receiver) {const res = Reflect.get(target, key, receiver)
      if (typeof res === 'object' && res !== null) {return myReactive(obj)
      }
      return res
    }
  })
}
const arr = myReactive([obj])

console.log(arr.includes(obj))
console.log(arr.indexOf(obj))
console.log(arr.lastIndexOf(obj))

当代码执行后,三个打印均为 false,但依照reactive 的逻辑,这三个打印应该打印 true。为什么会呈现这个问题呢?当调用includesindexOflastIndexOf 这些办法时,会遍历 arr,遍历arr 的过程取到的是 reactive 对象,如果拿这个 reactive 对象和 obj 原始对象比拟,必定找不到,所以须要重写这三个办法。

pushpopshiftunshiftsplice这些办法为什么要非凡解决呢?认真看这几个办法的执行,都会扭转数组的长度。以 push 为例,咱们查看 ECMAScript 对 push 的执行流程阐明:

在第二步中会读取数组的 length 属性,在第六步会设置 length 属性。咱们晓得在属性的读取过程中会进行依赖的收集,在属性的批改过程中会触发依赖(执行effect.run)。如果依照这样的逻辑会产生什么问题呢?咱们还是以一个例子阐明:

const arr = reactive([])
effect(() => {arr.push(1)
})

当向 arr 中进行 push 操作,首先读取到 arr.length,将length 对应的依赖 effect 收集起来,因为 push 操作会设置 length,所以在设置length 的过程中会触发 length 的依赖,执行 effect.run(),而在effect.run() 中会执行 this.fn(),又会调用arr.push 操作,这样就会造成一个死循环。

为了解决这两个问题,须要重写这几个办法。

arrayInstrumentations

const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()

function createArrayInstrumentations() {const instrumentations: Record<string, Function> = {}
  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {instrumentations[key] = function (this: unknown[], ...args: unknown[]) {const arr = toRaw(this) as any
      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}
    }
  })
  
  ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      // 暂停依赖收集
      // 因为 push 等操作是批改数组的,所以在 push 过程中不进行依赖的收集是正当的,只有它可能触发依赖就能够
      pauseTracking()
      const res = (toRaw(this) as any)[key].apply(this, args)
      resetTracking()
      return res
    }
  })
  return instrumentations
}

回到 get 捕捉器中,解决玩数组的几个非凡办法后,会应用 Reflect.get 获取后果 res。如果ressymbol类型,并且 keySymbol内置的值,间接返回 res;如果res 不是 symbol 类型,且 key 不再 __proto__(防止对原型进行依赖追踪)、__v_isRef__isVue 中。

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

// builtInSymbols: new Set(Object.getOwnPropertyNames(Symbol).map(key => Symbol[key]).filter(val => typeof val === 'symbol'))
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {return res}

如果不是只读响应式,就能够调用 track 进行依赖的收集。

if (!isReadonly) {track(target, TrackOpTypes.GET, key)
}

为什么非只读状况才收集依赖?

因为对于只读的响应式数据,是无奈对其进行批改的,所以收集它的依赖时没有用的,只会造成资源的节约。

如果是浅层响应式,返回res

if (shallow) {return res}

如果 resreftarget不是数组的状况下,会主动解包。

if (isRef(res)) {const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
  // 如果 target 不是数组或 key 不是整数,主动解包
  return shouldUnwrap ? res.value : res
}

如果 resObject,进行深层响应式解决。从这里就能看出,Proxy是懈怠式的创立响应式对象,只有拜访对应的key,才会持续创立响应式对象,否则不必创立。

if (isObject(res)) {return isReadonly ? readonly(res) : reactive(res)
}

最初,返回res

return res
set 捕捉器

set捕捉器能够捕捉obj.str=''arr[0]=1arr.length=2Reflect.set()Object.create(obj).foo ='foo'(批改继承者的属性)操作。

const set = /*#__PURE__*/ createSetter()

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {let oldValue = (target as any)[key]
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {return false}
    if (!shallow && !isReadonly(value)) {if (!isShallow(value)) {value = toRaw(value)
        oldValue = toRaw(oldValue)
      }
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {// in shallow mode, objects are set as-is regardless of reactive or not}

    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) {trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

set捕捉器通过一个 createSetter 函数创立。createSetter接管一个 shallow 参数,返回一个function

set拦截器中首先获取旧值。如果旧值是只读的 ref 类型,而新的值不是ref,则返回false,不容许批改。

let oldValue = (target as any)[key]
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {return false}

// 如果不是浅层响应式并且新的值不是 readonly
if (!shallow && !isReadonly(value)) {
  // 新值不是浅层响应式,新旧值取其对应的原始值
  if (!isShallow(value)) {value = toRaw(value)
    oldValue = toRaw(oldValue)
  }
  // 如果 target 不是数组并且旧值是 ref 类型,新值不是 ref 类型,间接批改 oldValue.value 为 value
  if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }
} else {
  // in shallow mode, objects are set as-is regardless of reactive or not
  // 如果是浅层响应式,对象按原样设置
}

为什么须要取新值和旧值的原始值?

防止设置属性的过程中造成原始数据的净化。来看上面一个例子:

const obj1 = {}
const obj2 = {a: obj1}
const obj2Reactive = reactive(obj2)

obj2Reactive.a = reactive(obj1)

console.log(obj2.a === obj1) // true

如果咱们不对 value 取原始值,在批改 obj2Reactivea属性时,会将响应式对象增加到 obj2 中,如此原始数据 obj2 中会被混入响应式数据,原始数据就被净化了,为了防止这种状况,就须要取 value 的原始值,将 value 的原始值增加到 obj2 中。

那为什么对 oldValue 取原始值,因为在后续批改操作触发依赖前须要进行新旧值的比拟时,而在比拟时,咱们不可能拿响应式数据与原始数据进行比拟,咱们须要拿新值和旧值的原始数据进行比拟,只有新值与旧值的原始数据不同,才会触发依赖。

接下来就是调用 Reflect.set 进行赋值。

// key 是不是 target 自身的属性
const hadKey =
  isArray(target) && isIntegerKey(key)
    ? Number(key) < target.length
    : hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)

而后触发依赖。

// 对于处在原型链上的 target 不触发依赖
if (target === toRaw(receiver)) {
  // 触发依赖,依据 hadKey 值决定是新增属性还是批改属性
  if (!hadKey) {trigger(target, TriggerOpTypes.ADD, key, value)
  } else if (hasChanged(value, oldValue))  // 如果是批改操作,比拟新旧值
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
}
// 返回 result
return result
deleteProperty 捕捉器

deleteProperty捕捉器用来捕捉 delete obj.strReflect.deletedeleteProperty 操作。

function deleteProperty(target: object, key: string | symbol): boolean {
  // key 是否是 target 本身的属性
  const hadKey = hasOwn(target, key)
  // 旧值
  const oldValue = (target as any)[key]
  // 调用 Reflect.deleteProperty 从 target 上删除属性
  const result = Reflect.deleteProperty(target, key)
  // 如果删除胜利并且 target 本身有 key,则触发依赖
  if (result && hadKey) {trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  // 返回 result
  return result
}
has 捕捉器

has捕捉器能够捕捉 for...in...key in objReflect.has() 操作。

function has(target: object, key: string | symbol): boolean {const result = Reflect.has(target, key)
  // key 不是 symbol 类型或不是 symbol 的内置属性,进行依赖收集
  if (!isSymbol(key) || !builtInSymbols.has(key)) {track(target, TrackOpTypes.HAS, key)
  }
  return result
}
ownKeys 捕捉器

ownKeys捕捉器能够捕捉 Object.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Reflect.ownKeys() 操作

function ownKeys(target: object): (string | symbol)[] {
  // 如果 target 是数组,收集 length 的依赖
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

其余 reactive

除了 reactivereadonlyshallowReadonlyshallowReactive 均是通过 createReactiveObject 创立的。不同是传递的参数不同。

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
  )
}

export function shallowReactive<T extends object>(target: T): ShallowReactive<T> {
  return createReactiveObject(
    target,
    false,
    shallowReactiveHandlers,
    shallowCollectionHandlers,
    shallowReactiveMap
  )
}

这里次要看一下 readonlyHandlers 的实现。

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

因为被 readonly 解决的数据不会被批改,所以所有的批改操作都不会被容许,批改操作不会进行意味着也就不会进行依赖的触发,对应地也就不须要进行依赖的收集,所以 ownKeyshas 也就没必要拦挡了。

对于汇合的解决将在前面文章持续剖析。

退出移动版