petite-vue原始碼剖析-逐行解讀@vue-reactivity之Map和Set的reactive

肥仔John發表於2022-04-15

本篇我們會繼續探索reactive函式中對Map/WeakMap/Set/WeakSet物件的代理實現。

Map/WeakMap/Set/WeakSet的操作

由於WeakMap和WeakSet分別是Map和Set的不影響GC執行垃圾回收的版本,這裡我們只研究Map和Set即可。

Set的屬性和方法

  • size: number 為訪問器屬性(accessor property),返回Set物件中的值的個數
  • add(value: any): Set 向Set物件隊尾新增一個元素
  • clear(): void 移除Set物件內所有元素
  • delete(value: any): boolean 移除Set中與入參值相同的元素,移除成功則返回true
  • has(value: any): boolean 判斷Set中是否存在與入參值相同的元素
  • values(): Iterator 返回一個新的迭代器物件,包含Set物件中按插入順序排列的所有元素
  • keys(): Iteratorvalues(): Iterator一樣的功效
  • @@iteratorvalues(): Iterator一樣的功效,for of中呼叫
  • entries(): Iterator 返回一個新的迭代器物件,包含Set物件中按插入順序排列的所有元素,但為與Map使用一致每次迭代返回的內容為[value, value]
  • forEach(callbackFn: { (value: any, set: Set) => any } [, thisArg]) 按插入順序遍歷Set物件的每一個元素

Map的屬性和方法

  • size: number 為訪問器屬性(accessor property),返回Set物件中的值的個數
  • set(key: any, value: any): Map 向Map物件新增或更新一個指定鍵的值
  • clear(): void 移除Map物件內所有鍵值對
  • delete(key: any): boolean 移除Map物件中指定的鍵值對,移除成功則返回true
  • has(key: any): boolean 判斷Map中是否存在鍵與入參值相同的鍵值對
  • values(): Iterator 返回一個新的迭代器物件,包含Map物件中按插入順序排列的所有值
  • keys(): Iterator 返回一個新的迭代器物件,包含Map物件中按插入順序排列的所有鍵
  • @@iteratorentries(): Iterator一樣的功效,for of中呼叫
  • entries(): Iterator 返回一個新的迭代器物件,包含Map物件中按插入順序排列的所有鍵值對
  • forEach(callbackFn: { (value: any, key: any, map: Map) => any } [, thisArg]) 按插入順序遍歷Map物件的每一個鍵值對
  • get(key: any): any 返回Map物件中指定鍵對應的值,若沒有則返回undefined

逐行看程式碼我是認真的

// reactive.ts

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

由於Map/Set不像Object或Array那樣可直接通過屬性訪問的方式獲取其中的元素,而是通過add,has,delete操作,因此需要像處理Array的slice等方法那樣代理Map/Set的這些方法。

// collectionHandlers.ts

type MapTypes = Map<any, any> | WeakMap<any, any>
type SetTypes = Set<any, any> | WeakSet<any, any>

// 代理Map/Set原生的方法
// 沒有代理返回迭代器的方法??
const mutableInstrumentations = {
  get(this: MapTypes, key: unknown) {
    return get(this, key)
  }
  get size() {
    // 原生的size屬性就是一個訪問器屬性
    return size(this as unknown as IterableCollections)
  },
  has,
  add,
  set,
  delete: deleteEntry, // delete 是關鍵字不能作為變數或函式名稱
  clear,
  forEach: createForEach(false, false)
}

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = 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
    }

    // 代理Map/WeakMap/Set/WeakSet的內建方法
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

TypeScript小課堂as斷言——this as unknown as IterableCollections
在TypeScript中可通過型別宣告定義變數的型別(其中包含複合型別),而型別推導則可以根據賦值語句中右側字面量推匯出變數的實際型別,或通過當前變數使用的場景推匯出當前實際型別(尤其是定義為複合型別)。但有時無法通過當前使用場景執行精確的型別推導,這時開發者可以通過as斷言告知TypeScript編譯器該變數當前使用範圍的資料型別(要相信自己一定比編譯器更瞭解自己的程式碼:D)。
那麼as unknown即表示將型別修改為unknown,那麼型別為unknown是表示什麼呢?unknown是TypeScript3.0引入的top type(任何其他型別都是它的subtype),意在提供一種更安全的方式替代any型別(any型別是top type也是bottom type,使用它意味和繞過型別檢查),具有如下特點:

  1. 任何其它型別都可以賦值給unknown型別的變數
  2. unknown型別的變數只能賦值給anyunknown型別的變數
  3. 如果不對unknown型別的變數執行型別收縮,則無法執行其它任何操作
// 1. 任何其它型別都可以賦值給`unknown`型別的變數 
let uncertain: unknown = 'Hello'
uncertain = 12
uncertain = { hello: () => 'Hello' }

// 2.`unknown`型別的變數只能賦值給`any`或`unknown`型別的變數 
let uncertain: unknown = 'Hello'
let noSure: any = uncertain
let notConfirm: unknown = uncertain

// 3. 如果不對`unknown`型別的變數執行型別收縮,則無法執行其它任何操作
let uncertain = { hello: () => 'Hello' }
uncertain.hello() // 編譯報錯 
// 通過斷言as收縮型別
(uncertain as {hello: () => string}).hello()

let uncertain: unknown = 'Hello'
// 通過typeof或instanceof收縮型別
if (typeof uncertain === 'string') {
  uncertain.toLowerCase()
}

那麼as unknown後的as IterableCollections意圖就十分明顯了,就是對變數進行型別收縮。this as unknown as IterableCollections其實就是as IterableCollections啦。

然後我們逐一看看代理方法的實現吧

Mapget方法

get方法只有Map物件擁有,因此其中主要思路是從Map物件中獲取值,跟蹤鍵值變化後將值轉換為響應式物件返回即可。
但由於要處理readonly(reactive(new Map()))這一場景,新增了很多一時讓人看不懂的程式碼而已。

const getProto = <T extends CollectionTypes>(v: T): any => Reflect.getProrotypeOf(v)

// 代理Map/WeakMap的get方法
function get(
  target: MapTypes, // 指向this,由於Map物件已經被代理,因此this為代理代理
  key: unknown,
  isReadonly = false,
  isShallow = false
) {
  /**
   * 1. 針對readonly(reactive(new Map()))的情況,
   *    target獲取的是代理物件,而rawTarget的是Map物件
   * 2. 針對reactive(new Map())的情況,
   *    target和rawTarget都是指向Map物件
   */ 
  target = (target as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  /**
   * 若key為代理物件,那麼被代理物件和代理物件的鍵都會被跟蹤,即
   * const key = { value: 'foo' }
   * const pKey = reactive(key), 
   * const kvs = reactive(new Map())
   * kvs.set(pKey, 1)
   * 
   * effect(() => {
   *   console.log('pKey', kvs.get(pKey))
   * })
   * effect(() => {
   *   console.log('key', kvs.get(key))
   * })
   * 
   * kvs.set(pKey, 2)
   * // 回顯 pkey 2 和 key 2
   * kvs.set(key, 3)
   * // 回顯 key 2
   */  
  const rawKey = toRaw(key)
  if (key !== rawKey) {
    !isReadonly && track(rawTraget, TrackOpTypes.GET, key)
  }
  !isReadonly && track(rawTraget, TrackOpTypes.GET, rawKey)

  // 獲取Map原型鏈上的has方法用於判斷獲取成員是否存在於Map物件上
  const { has } = getProto(rawTarget)
  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
  /**
   * Map物件中存在則從Map物件或代理物件上獲取值並轉換為響應式物件返回。
   * 針對readonly(reactive(new Map()))為什麼是從響應物件上獲取值,而不是直接從Map物件上獲取值呢?
   * 這是為了保持返回的值的結構,從響應式物件中獲取值是響應式物件,在經過readonly的處理則返回的值就是readonly(reactive({value: 'foo'}))。
   */ 
  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) {
    /**
     * 針對readonly(reactive(new Map())),即使沒有匹配的鍵值對,也要跟蹤對響應式物件某鍵的依賴資訊
     * const state = reactive(new Map())
     * const readonlyState = readonly(state)
     * 
     * effect(() => {
     *  console.log(readonlyState.get('foo'))
     * })
     * // 回顯 undefined
     * state.set('foo', 1)
     * // 回顯 1
     */
    target.get(key)
  }

  // 啥都沒有找到就預設返回undefined,所以啥都不用寫
}

MapSetsize訪問器屬性

function size(target: IterableCollections, isReadonly = false) {
  // 針對readonly(reactive(new Map())) 或 readonly(reactive(new Set()))只需獲取響應式物件即可,因此reactive物件也會對size的訪問進行相同的操作。
  target = (target as any)[RectiveFlags.RAW]
  // 跟蹤ITERATE_KEY即所有修改size的操作均會觸發訪問size屬性的副作用函式
  !iReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  /**
   * 由於size為訪問器屬性因此若第三個引數傳遞receiver(響應式物件),而響應式物件並沒有size訪問器屬性需要訪問的屬性和方法,則會報異常``。因此需要最終將Map或Set物件作為size訪問器屬性的this變數。
   */
  return Reflect.get(target, 'size', target)
}

MapSethas方法

function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
  const target = (this as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  // 和get方法代理一樣,若key為代理物件則代理物件或被代理物件作為鍵的鍵值對發生變化都會觸發訪問has的副作用函式
  if (key !== rawKey) {
    !isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
  }
  !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)

  return key === rawKey
    ? target.has(key)
    : target.has(key) || target.has(rawKey)
}

Setadd方法

function add(this: SetTypes, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  // 當Set物件中沒有該元素時則觸發依賴ITERATE_KEY的副作用函式,因此ADD操作會影響Set物件的長度
  if (!hadKey) {
    target.add(value)
    trigger(target, TriggerOpTypes.ADD, value, value)
  }

  return this
}

Mapset方法

function set(this: MapTypes, key: unknown, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  const { has, get } = getProto(target)

  // 分別檢查代理和非代理版本的key是否存在於Map物件中
  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target.key)
  }

  const oldValue = get.call(target, key)
  target.set(key, value)
  if (!hadKey) {
    // 當Map物件中沒有該元素時則觸發依賴ITERATE_KEY的副作用函式,因此ADD操作會影響Map物件的長度
    trigger(target, TriggerOpTypes.ADD, key, value)
  }
  else if (hasChanged(value, oldValue)) {
    // 如果新舊值不同則觸發修改,依賴該鍵值對的副作用函式將被觸發
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
}

注意:gethas方法中會同時跟蹤代理和非代理版本的鍵對應的元素變化,而set方法則只會觸發查詢到的代理或非代理版本的鍵對應的元素變化。

deleteEntry方法

function deleteEntry(this: CollectionTypes, key: unknown) {
  const target = toRaw(this)
  const { has, get } = getProto(target)
  let hadKey = has.call(target, key)
  // 分別檢查代理和非代理版本的key是否存在於Map/Set物件中
  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target.key)
  }

  // 如果當前操作的是Map物件則獲取舊值
  const oldValue = get ? get.call(target, key) : undefined
  const result = target.delete(key)
  if (hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

注意:gethas方法中會同時跟蹤代理和非代理版本的鍵對應的元素變化,而deleteEntry方法則只會觸發查詢到的代理或非代理版本的鍵對應的元素變化。

MapSetclear方法

function clear(this: IterableCollections) {
  const target = toRaw(this)
  const hadItems = target.size !== 0
  const oldTarget = undefined
  const result = target.clear()
  if (hadItems) {
    trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
  }
  return result
}

MapSetforEach方法

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) => {
      // 將key和value都轉換為代理物件
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    })
  }
}

由於forEach會遍歷所有元素(Map物件則是所有鍵值對),因此跟蹤ITERATE_KEY即Map/Set物件元素個數發生變化則觸發forEach函式的執行。

迭代器物件相關方法

至此我們還沒對entries,values,keys@@iterator這些返回迭代器的物件方法進行代理,而原始碼中則在最後為mutableInstrumentations新增這些方法的代理。

const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator/*就是@@iterator*/]
iteratorMethods.forEach(method => {
  mutableInstrumentations[method as string] = createIterableMethod(
    method,
    false,
    false
  )
})
function createIterableMethod(
  method: string | symbol,
  isReadonly: boolean,
  isShallow: boolean
) {
  return function(
    this: IterableCollections,
    ...args: unknown[]
  ): Iterable & Iterator {
    /**
     * 1. 針對readonly(reactive(new Map()))的情況,
     *    target獲取的是代理物件,而rawTarget的是Map或Set物件
     * 2. 針對reactive(new Map())的情況,
     *    target和rawTarget都是指向Map或Set物件
     */ 
    const target = (this as any)[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)

    const targetIsMap = isMap(rawTarget)
    const isPair = method === 'entries' || (method === Symbol.iterator && targetIsMap)
    /**
     * 當呼叫的是Map物件的keys方法,副作用函式並沒有訪問值物件,即副作用函式只依賴Map物件的鍵而沒有依賴值。
     * 而鍵只能增加或刪除,值可增加、刪除和修改,那麼此時當且僅當鍵增刪即size屬性發生變化時才會觸發副作用函式的執行。
     * 若依賴值,那麼修改其中一個值也會觸發副作用函式執行。
     */
    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() {
        const { value, done } = innerIterator.next()
        return done
          ? { value, done }
          : {
            value: isPair ? [wrap(value[0], wrap(value[1]))] : wrap(value),
            done
          }
      },
      // 可迭代協議
      [Symbol.iterator]() {
        return this
      }
    }
  }
}

可迭代協議(iterable protocol)

可迭代協議(iterable protocol),用於建立迭代器(iterator)。
如下內建型別都實現了可迭代協議:

  • 字串
  • 陣列
  • Set
  • Map
  • arguements物件
  • NodeList等DOM集合型別

下面的語言特性將會接收可迭代協議返回的迭代器

  • for...of迴圈
  • 資料解構(const [a, b] = [1, 2])
  • 擴充套件操作符(const a = [1,2], b = [...a])
  • Array.from()
  • 建立Set
  • 建立Map
  • Promise.all()接受可迭代物件
  • Promise.race()接受可迭代物件
  • yield*操作符

讓物件支援可迭代協議其實很簡單,只需實現返回迭代器的[Symbol.iterator]方法即可。JavaScript Plain Old Object預設並沒有支援可迭代協議,那麼我們可以自行實現以下:

const iterablizeKeys = (obj: {}) => {
  if (!obj[Symbol.iterator]) {
    obj[Symbol.iterator] = () => {
      const keys = Object.keys(obj) as const
      let i = 0

      // 返回一個迭代器
      return {
        next() {
          return { value: keys[i++], done: i > keys.length }
        }
      }
    }
  }

  return obj
} 

const iterableObj = iterablizeKeys({a: 1, b: 2})
for (let item of iterableObj) {
  console.log(item)
}
// 回顯 a 
// 回顯 b
Array.from(iterableObj) // 返回 ['a', 'b']

迭代器協議(iterator protocol)

迭代器協議(iterator protocol),提供不接受任何引數並返回IteratorResult物件的next方法,而IteratorResult物件包含指向當前元素的value屬性和表示迭代是否已結束的done屬性,當done屬性值為true時表示迭代已結束。
迭代器協議的實現正如上面可迭代協議的示例中那樣,不過我們還可以將可迭代協議和迭代物件在同一個物件上實現。

const iterablizeKeys = (obj: {}) => {
  if (!obj[Symbol.iterator]) {
    let iteratorState = {
      keys: []
      i: 0
    }
    // 迭代器協議
    obj.next = () => ({ value: iteratorState.keys[iteratorState.i++], done: iteratorState.i > iteratorState.key.length })

    // 可迭代協議
    obj[Symbol.iterator] = () => {
      iteratorState.keys = Object.keys(obj) as const
      iteratorState.i = 0

      // 返回一個迭代器
      return this
    }
  }

  return obj
} 

const iterableObj = iterablizeKeys({a: 1, b: 2})
for (let item of iterableObj) {
  console.log(item)
}
// 回顯 a 
// 回顯 b
Array.from(iterableObj) // 返回 ['a', 'b']

總結

本篇我們通過逐行閱讀原始碼瞭解到reactive如何處理Map和Set物件了,下一篇我們將開始以effect為入口進一步瞭解副作用函式是如何通過tracktrigger記錄依賴和觸發的。
尊重原創,轉載請註明來自:https://www.cnblogs.com/fsjoh...肥仔John

相關文章