【vue3源碼】八畴椰、reactive——Collection的響應(yīng)式實(shí)現(xiàn)

【vue3源碼】八臊诊、reactive——Collection的響應(yīng)式實(shí)現(xiàn)

參考代碼版本:vue 3.2.37

官方文檔:https://vuejs.org/

前文中我們分析了reactive對(duì)Object類型的數(shù)據(jù)處理,這篇文章繼續(xù)介紹對(duì)集合的處理斜脂。

mutableCollectionHandlers

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

對(duì)于集合抓艳,讀取操作和修改操作都是通過調(diào)用方法(size除外)進(jìn)行,所以只需要捕獲其get方法即可帚戳。get捕獲器通過createInstrumentationGetter函數(shù)生成玷或。

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函數(shù)接收兩個(gè)參數(shù):isReadonly(是否是只讀響應(yīng)式)儡首、shallow(是否是淺層響應(yīng)式)。

首先根據(jù)isReadonlyshallow的值偏友,獲取對(duì)應(yīng)的instrumentations蔬胯。

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

instrumentations是什么呢?

instrumentations就是個(gè)對(duì)象约谈,它通過createInstrumentations生成,內(nèi)部重寫了集合的一些方法犁钟。createInstrumentations方法創(chuàng)建四個(gè)instrumentations

  • mutableInstrumentations:處理可修改的響應(yīng)式集合數(shù)據(jù)
  • readonlyInstrumentations:處理只讀的響應(yīng)式集合數(shù)據(jù)
  • shallowInstrumentations:處理淺層響應(yīng)式集合數(shù)據(jù)
  • shallowReadonlyInstrumentations:處理淺層只讀響應(yīng)式集合數(shù)據(jù)
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函數(shù)可接收四個(gè)參數(shù):target目標(biāo)集合棱诱、keyisReadonly是否是只讀響應(yīng)式涝动、isShallow是否是淺層響應(yīng)式迈勋。

function get(
  target: MapTypes,
  key: unknown,
  isReadonly = false,
  isShallow = false
) {
  // readonly(reactive(Map)) 應(yīng)該返回值的readonly + reactive
  target = (target as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  // 如果key與其原始對(duì)象不一致,說明key是響應(yīng)式數(shù)據(jù)
  if (key !== rawKey) {
    // 如果不是只讀的話醋粟,收集key的依賴
    !isReadonly && track(rawTarget, TrackOpTypes.GET, key)
  }
  // 收集key的原始值的依賴
  !isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
  const { has } = getProto(rawTarget)
  // 確定包裝函數(shù)
  // 如果是淺層響應(yīng)式靡菇,包裝函數(shù)返回入?yún)ⅲ?value) => value
  // 如果是只讀的響應(yīng)式,包裝函數(shù)就是會(huì)將value轉(zhuǎn)為readonly
  // 否則使用reactive將value轉(zhuǎn)為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是個(gè)響應(yīng)式數(shù)據(jù)米愿,那么繼續(xù)調(diào)用target.get厦凤。例如readonly(reactive(Map))
    target.get(key)
  }
}

可以發(fā)現(xiàn),如果通過get方法獲取一個(gè)響應(yīng)式數(shù)據(jù)對(duì)應(yīng)的值時(shí)育苟,會(huì)有兩次依賴的收集较鼓,為什么這么做呢?

其實(shí)這樣做的目的是使通過響應(yīng)式數(shù)據(jù)的原始值設(shè)置Map時(shí)违柏,能夠照常觸發(fā)依賴博烂。例如下面這個(gè)例子:

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

以上代碼會(huì)依次打印a b c。無論通過key還是key的原始值進(jìn)行修改Map漱竖,都能夠觸發(fā)依賴禽篱。

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是響應(yīng)式對(duì)象,收集依賴
  if (key !== rawKey) {
    !isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
  }
  // 收集依賴key的原始值對(duì)應(yīng)依賴
  !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的原始對(duì)象
  value = toRaw(value)
  const target = toRaw(this)
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  // target中不存在value時(shí)馍惹,才能觸發(fā)依賴
  if (!hadKey) {
    target.add(value)
    trigger(target, TriggerOpTypes.ADD, value, value)
  }
  // 返回this躺率,因?yàn)镾et的add操縱可以鏈?zhǔn)讲僮?  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值是不是響應(yīng)數(shù)據(jù)
  // 如果不存在万矾,再檢查是否存在key的原始數(shù)據(jù)
  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,因?yàn)镸ap的set操縱可以鏈?zhǔn)讲僮?  return this
}

為什么兩次檢查key的存在勤众?

保證使用響應(yīng)式數(shù)據(jù)作為key向Map中添加數(shù)據(jù)和使用響應(yīng)式數(shù)據(jù)的原始值作為key向Map中修改數(shù)據(jù)時(shí)舆绎,修改的是同一個(gè)key的數(shù)據(jù)。例如下面這個(gè)例子

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
  // 進(jìn)行刪除
  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操作不會(huì)觸發(fā)依賴
  if (hadItems) {
    trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
  }
  return result
}

forEach

forEach函數(shù)由createForEach創(chuàng)建。createForEach接收兩個(gè)參數(shù):isReadonly(是否是只讀響應(yīng)式)努溃、isShallow(是否是淺層響應(yīng)式)硫嘶。

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的響應(yīng)性質(zhì)與this保持一致
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    })
  }
}

shallowInstrumentations

shallowInstrumentationsshallowInstrumentations相似梧税,只不過在生成getcreateForEach函數(shù)時(shí)沦疾,傳遞的參數(shù)不一樣。

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對(duì)象是用來處理只讀響應(yīng)式數(shù)據(jù)的第队,所以所有可修改集合的操作都會(huì)通過操作失敗哮塞。這些可以修改集合的操作函數(shù)都會(huì)被一個(gè)createReadonlyMethod函數(shù)生成。

createReadonlyMethod函數(shù)接接收一個(gè)type參數(shù)凳谦,并返回一個(gè)匿名函數(shù)忆畅。

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

除了重寫了以上幾個(gè)方法外,還對(duì)keys尸执、values等方法也進(jìn)行了重寫:

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

keys家凯、valuesentries如失、Symbol.iterator的重寫函數(shù)均通過一個(gè)createIterableMethod函數(shù)生成绊诲。

Symbol.iterator是什么?

集合的Symbol.iterator函數(shù)可以用來獲取迭代器對(duì)象褪贵,正是因?yàn)榧蠈?shí)現(xiàn)了Symbol.iterator方法驯镊,所以可以使用for...of進(jìn)行迭代。而這里需要重寫Symbol.iterator方法竭鞍,目的是為了實(shí)現(xiàn)使用for...of迭代代理對(duì)象板惑。如下:

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

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

createIterableMethod接收三個(gè)參數(shù):methodisReadonly偎快、isShallow冯乘。

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)
    // 根據(jù)isPair判斷迭代時(shí)的參數(shù)
    const isPair =
      method === 'entries' || (method === Symbol.iterator && targetIsMap)
    const isKeyOnly = method === 'keys' && targetIsMap
    // 獲取迭代器
    const innerIterator = target[method](...args)
    // 包裝函數(shù)
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    // 依賴收集
    !isReadonly &&
      track(
        rawTarget,
        TrackOpTypes.ITERATE,
        isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
      )
    // 返回一個(gè)同時(shí)滿足迭代器協(xié)議和可迭代協(xié)議的對(duì)象
    return {
      // 迭代器協(xié)議
      next() {
        // 調(diào)用原始對(duì)象的迭代器的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
            }
      },
      // 實(shí)現(xiàn)可迭代協(xié)議晒夹,意味著可以使用for...of迭代map.keys/values/entries()
      [Symbol.iterator]() {
        return this
      }
    }
  }
}

總結(jié)

reactive通過Proxy代理原始對(duì)象裆馒,通過攔截代理對(duì)象的操作進(jìn)行依賴的收集與觸發(fā)。當(dāng)對(duì)代理對(duì)象進(jìn)行讀取操作時(shí)丐怯,進(jìn)行依賴的收集喷好;對(duì)代理對(duì)象進(jìn)行修改操作則觸發(fā)依賴,無論是讀取操作還是修改操作读跷,其實(shí)都是操作的原始對(duì)象梗搅,為了在執(zhí)行修改操作時(shí)不污染原始對(duì)象,都會(huì)先調(diào)用toRaw獲取value的原始值,然后再進(jìn)行修改无切。

reactive的實(shí)現(xiàn)是懶惰的荡短,如果不訪問代理對(duì)象的屬性,那么永遠(yuǎn)不會(huì)將代理對(duì)象的屬性轉(zhuǎn)為代理對(duì)象哆键。

reactive流程:

reactive.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末掘托,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子籍嘹,更是在濱河造成了極大的恐慌闪盔,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辱士,死亡現(xiàn)場(chǎng)離奇詭異泪掀,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)识补,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門族淮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辫红,“玉大人凭涂,你說我怎么就攤上這事√蓿” “怎么了切油?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)名惩。 經(jīng)常有香客問我澎胡,道長(zhǎng),這世上最難降的妖魔是什么娩鹉? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任攻谁,我火速辦了婚禮,結(jié)果婚禮上弯予,老公的妹妹穿的比我還像新娘戚宦。我一直安慰自己,他們只是感情好锈嫩,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布受楼。 她就那樣靜靜地躺著,像睡著了一般呼寸。 火紅的嫁衣襯著肌膚如雪艳汽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天对雪,我揣著相機(jī)與錄音河狐,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛甚牲,可吹牛的內(nèi)容都是我干的义郑。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼丈钙,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼非驮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起雏赦,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤劫笙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后星岗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體填大,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年俏橘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了允华。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡寥掐,死狀恐怖靴寂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情召耘,我是刑警寧澤百炬,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站污它,受9級(jí)特大地震影響剖踊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜衫贬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一德澈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧固惯,春花似錦梆造、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至供常,卻和暖如春摊聋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背栈暇。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工麻裁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓煎源,卻偏偏與公主長(zhǎng)得像色迂,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子手销,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容