关于前端:vue3源码八reactiveCollection的响应式实现

5次阅读

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

【vue3 源码】八、reactive——Collection 的响应式实现

参考代码版本:vue 3.2.37

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

前文中咱们剖析了 reactiveObject类型的数据处理,这篇文章持续介绍对汇合的解决。

mutableCollectionHandlers

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

对于汇合,读取操作和批改操作都是通过调用办法(size除外)进行,所以只须要捕捉其 get 办法即可。get捕捉器通过 createInstrumentationGetter 函数生成。

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
    ? readonlyInstrumentations
    : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    // 解决非凡的 key
    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
    )
  }
}

createInstrumentationGetter函数接管两个参数:isReadonly(是否是只读响应式)、shallow(是否是浅层响应式)。

首先依据 isReadonlyshallow的值,获取对应的instrumentations

const instrumentations = shallow
  ? isReadonly
    ? shallowReadonlyInstrumentations
    : shallowInstrumentations
  : isReadonly
  ? readonlyInstrumentations
  : mutableInstrumentations

instrumentations是什么呢?

instrumentations就是个对象,它通过 createInstrumentations 生成,外部重写了汇合的一些办法。createInstrumentations办法创立四个instrumentations

  • mutableInstrumentations:解决可批改的响应式汇合数据
  • readonlyInstrumentations:解决只读的响应式汇合数据
  • shallowInstrumentations:解决浅层响应式汇合数据
  • shallowReadonlyInstrumentations:解决浅层只读响应式汇合数据
const [
  mutableInstrumentations,
  readonlyInstrumentations,
  shallowInstrumentations,
  shallowReadonlyInstrumentations
] = /* #__PURE__*/ createInstrumentations()

mutableInstrumentations

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

get函数可接管四个参数:target指标汇合、keyisReadonly是否是只读响应式、isShallow是否是浅层响应式。

function get(
  target: MapTypes,
  key: unknown,
  isReadonly = false,
  isShallow = false
) {// readonly(reactive(Map)) 应该返回值的 readonly + reactive
  target = (target as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  // 如果 key 与其原始对象不统一,阐明 key 是响应式数据
  if (key !== rawKey) {
    // 如果不是只读的话,收集 key 的依赖
    !isReadonly && track(rawTarget, TrackOpTypes.GET, key)
  }
  // 收集 key 的原始值的依赖
  !isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
  const {has} = getProto(rawTarget)
  // 确定包装函数
  // 如果是浅层响应式,包装函数返回入参:(value) => value
  // 如果是只读的响应式,包装函数就是会将 value 转为 readonly
  // 否则应用 reactive 将 value 转为 reactive
  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
  // target 的原始值本身蕴含 key 值
  if (has.call(rawTarget, key)) {return wrap(target.get(key)) 
  } else if (has.call(rawTarget, rawKey)) { // target 的原始值本身蕴含 key 的原始值
    return wrap(target.get(rawKey))
  } else if (target !== rawTarget) {// target 与原始值不同, 阐明 target 是个响应式数据,那么持续调用 target.get。例如 readonly(reactive(Map))
    target.get(key)
  }
}

能够发现,如果通过 get 办法获取一个响应式数据对应的值时,会有两次依赖的收集,为什么这么做呢?

其实这样做的目标是使通过响应式数据的原始值设置 Map 时,可能照常触发依赖。例如上面这个例子:

const key = ref('a')
const map = reactive(new Map())
map.set(key, 'a')

effect(() => {console.log(map.get(key))
})
map.set(key, 'b')
map.set(toRaw(key), 'c')

以上代码会顺次打印 a b c。无论通过key 还是 key 的原始值进行批改Map,都可能触发依赖。

size
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)
}
has
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {const target = (this as any)[ReactiveFlags.RAW]
  // 取 target/key 的原始值
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  // 如果 key 是响应式对象,收集依赖
  if (key !== rawKey) {!isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
  }
  // 收集依赖 key 的原始值对应依赖
  !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)
  return key === rawKey
    ? target.has(key)
    : target.has(key) || target.has(rawKey)
}
add
function add(this: SetTypes, value: unknown) {
  // 获取 value 与 this 的原始对象
  value = toRaw(value)
  const target = toRaw(this)
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  // target 中不存在 value 时,能力触发依赖
  if (!hadKey) {target.add(value)
    trigger(target, TriggerOpTypes.ADD, value, value)
  }
  // 返回 this,因为 Set 的 add 操纵能够链式操作
  return this
}
set
function set(this: MapTypes, key: unknown, value: unknown) {value = toRaw(value)
  const target = toRaw(this)
  const {has, get} = getProto(target)

  // 先查看 target 中是否存在 key,无论 key 值是不是响应数据
  // 如果不存在,再查看是否存在 key 的原始数据
  let hadKey = has.call(target, key)
  if (!hadKey) {key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {checkIdentityKeys(target, has, key)
  }

  const oldValue = get.call(target, key)
  target.set(key, value)
  // 如果不存在 key 阐明是新增操作,反之为批改操作
  if (!hadKey) {trigger(target, TriggerOpTypes.ADD, key, value)
  } else if (hasChanged(value, oldValue)) {trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  // 返回 this,因为 Map 的 set 操纵能够链式操作
  return this
}

为什么两次查看 key 的存在?

保障应用响应式数据作为 key 向 Map 中增加数据和应用响应式数据的原始值作为 key 向 Map 中批改数据时,批改的是同一个 key 的数据。例如上面这个例子

const key = reactive({})
const map = reactive(new Map())
map.set(toRaw(key), 'c')
map.set(key, 'b')

console.log(map.size) // 1
delete
function deleteEntry(this: CollectionTypes, key: unknown) {const target = toRaw(this)
  const {has, get} = getProto(target)
  let hadKey = has.call(target, key)
  // 如果 target 中没有 key,再寻找是否有 key 的原始值,与 set 雷同
  if (!hadKey) {key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {checkIdentityKeys(target, has, key)
  }

  const oldValue = get ? get.call(target, key) : undefined
  // 进行删除
  const result = target.delete(key)
  if (hadKey) {trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}
clear
function clear(this: IterableCollections) {const target = toRaw(this)
  const hadItems = target.size !== 0
  const oldTarget = __DEV__
    ? isMap(target)
      ? new Map(target)
      : new Set(target)
    : undefined
  // forward the operation before queueing reactions
  const result = target.clear()
  // 如果汇合中本就没有值,clear 操作不会触发依赖
  if (hadItems) {trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
  }
  return result
}
forEach

forEach函数由 createForEach 创立。createForEach接管两个参数:isReadonly(是否是只读响应式)、isShallow(是否是浅层响应式)。

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)
    return target.forEach((value: unknown, key: unknown) => {
      // 包装 value 及 key,使在 forEach 中拜访到的 key 与 value 的响应性质与 this 保持一致
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    })
  }
}

shallowInstrumentations

shallowInstrumentationsshallowInstrumentations 类似,只不过在生成 getcreateForEach函数时,传递的参数不一样。

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

readonlyInstrumentations

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

readonlyInstrumentations对象是用来解决只读响应式数据的,所以所有可批改汇合的操作都会通过操作失败。这些能够批改汇合的操作函数都会被一个 createReadonlyMethod 函数生成。

createReadonlyMethod函数接接管一个 type 参数,并返回一个匿名函数。

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

shallowReadonlyInstrumentations

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

除了重写了以上几个办法外,还对 keysvalues 等办法也进行了重写:

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

keysvaluesentriesSymbol.iterator的重写函数均通过一个 createIterableMethod 函数生成。

Symbol.iterator 是什么?

汇合的 Symbol.iterator 函数能够用来获取迭代器对象,正是因为汇合实现了 Symbol.iterator 办法,所以能够应用 for...of 进行迭代。而这里须要重写 Symbol.iterator 办法,目标是为了实现应用 for...of 迭代代理对象。如下:

const map = reactive(new Map([['a', 1], ['b', 2]]))

for(const [key, value] of map) {console.log(key, value)
}

createIterableMethod接管三个参数:methodisReadonlyisShallow

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)
    const targetIsMap = isMap(rawTarget)
    // 依据 isPair 判断迭代时的参数
    const isPair =
      method === 'entries' || (method === Symbol.iterator && targetIsMap)
    const isKeyOnly = method === 'keys' && targetIsMap
    // 获取迭代器
    const innerIterator = target[method](...args)
    // 包装函数
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    // 依赖收集
    !isReadonly &&
      track(
        rawTarget,
        TrackOpTypes.ITERATE,
        isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
      )
    // 返回一个同时满足迭代器协定和可迭代协定的对象
    return {
      // 迭代器协定
      next() {
        // 调用原始对象的迭代器的 next 办法获取 value 与 done
        const {value, done} = innerIterator.next()
        return done
          ? {value, done}
          : {
              // 包装 key、value
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
              done
            }
      },
      // 实现可迭代协定,意味着能够应用 for...of 迭代 map.keys/values/entries()
      [Symbol.iterator]() {return this}
    }
  }
}

总结

reactive通过 Proxy 代理原始对象,通过拦挡代理对象的操作进行依赖的收集与触发。当对代理对象进行读取操作时,进行依赖的收集;对代理对象进行批改操作则触发依赖,无论是读取操作还是批改操作,其实都是操作的原始对象,为了在执行批改操作时不净化原始对象,都会先调用 toRaw 获取 value 的原始值,而后再进行批改。

reactive的实现是懈怠的,如果不拜访代理对象的属性,那么永远不会将代理对象的属性转为代理对象。

reactive流程:

正文完
 0