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

【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流程:

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理