<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          聽說你很了解 Vue3 響應(yīng)式?

          共 36111字,需瀏覽 73分鐘

           ·

          2022-10-25 08:58

          前言

          << 溫馨提醒 >> 本文內(nèi)容偏干,建議邊喝水邊食用,如有不適請及時點贊!

          【A】:能不能說說 Vue3 響應(yīng)式都處理了哪些數(shù)據(jù)類型?都怎么處理的呀?

          【B】:能,只能說一點點...

          image.png

          【A】:...

          只要問到 Vue 相關(guān)的內(nèi)容,似乎總繞不過 響應(yīng)式原理 的話題,隨之而來的回答必然是圍繞著 Object.definePropertyProxy 來展開(即 Vue2Vue3),但若繼續(xù)追問某些具體實現(xiàn)是不是就倉促結(jié)束回答了(~~你跑我追,你不跑我還追~~)。

          本文就不再過多介紹 Vue2 中響應(yīng)式的處理,感興趣可以參考 **從 vue 源碼看問題 —— 如何理解 `Vue` 響應(yīng)式?**[2],但是會有簡單提及,下面就來看看 Vue3 中是如何處理 原始值、Object、Array、Set、Map 等數(shù)據(jù)類型的響應(yīng)式。

          1ACBCFF0.jpg

          Object.definePropertyProxy

          一切的一切還得從 Object.defineProperty 開始講起,那是一個不一樣的 API ... (~~bgm 響起,自行體會~~)

          Object.defineProperty

          Object.defineProperty(obj, prop, descriptor) 方法會直接在一個對象上定義一個 新屬性,或修改一個 對象現(xiàn)有屬性,并返回此對象,其參數(shù)具體為:

          • obj:要定義屬性的對象

          • prop:要定義或修改的 屬性名稱 或 **`Symbol`**[3]

          • descriptor:要定義或修改的 屬性描述符

          從以上的描述就可以看出一些限制,比如:

          • 目標是 對象屬性,不是 整個對象
          • 一次只能 定義或修改一個屬性
            • 當然有對應(yīng)的一次處理多個屬性的方法 `Object.defineProperties()`[4],但在 vue 中并不適用,因為 vue 不能提前知道用戶傳入的對象都有什么屬性,因此還是得經(jīng)過類似 Object.keys() + for 循環(huán)的方式獲取所有的 key -> value,而這其實是沒有必要使用 `Object.defineProperties()`[5]

          在 Vue2 中的缺陷

          Object.defineProperty() 實際是通過 定義修改 對象屬性 的描述符來實現(xiàn) 數(shù)據(jù)劫持,其對應(yīng)的缺點也是沒法被忽略的:

          • 只能攔截對象屬性的 getset 操作,比如無法攔截 delete、in、方法調(diào)用 等操作
          • 動態(tài)添加新屬性(響應(yīng)式丟失)
            • 保證后續(xù)使用的屬性要在初始化聲明 data 時進行定義
            • 使用 this.$set() 設(shè)置新屬性
          • 通過 delete 刪除屬性(響應(yīng)式丟失)
            • 使用 this.$delete() 刪除屬性
          • 使用數(shù)組索引 替換/新增 元素(響應(yīng)式丟失)
            • 使用 this.$set() 設(shè)置新元素
          • 使用數(shù)組 push、pop、shift、unshift、splice、sort、reverse原生方法 改變原數(shù)組時(響應(yīng)式丟失)
            • 使用 重寫/增強 后的 push、pop、shift、unshift、splice、sort、reverse 方法
          • 一次只能對一個屬性實現(xiàn) 數(shù)據(jù)劫持,需要遍歷對所有屬性進行劫持
          • 數(shù)據(jù)結(jié)構(gòu)復(fù)雜時(屬性值為 引用類型數(shù)據(jù)),需要通過 遞歸 進行處理

          【擴展】Object.definePropertyArray ?

          它們有啥關(guān)系,其實沒有啥關(guān)系,只是大家習慣性的會回答 Object.defineProperty 不能攔截 Array 的操作,這句話說得對但也不對。

          使用 Object.defineProperty 攔截 Array

          Object.defineProperty 可用于實現(xiàn)對象屬性的 getset 攔截,而數(shù)組其實也是對象,那自然是可以實現(xiàn)對應(yīng)的攔截操作,如下:

          Vue2 為什么不使用 Object.defineProperty 攔截 Array?

          尤大在曾在 GitHubIssue 中做過如下回復(fù):

          說實話性能問題到底指的是什么呢? 下面是總結(jié)了一些目前看到過的回答:

          • 數(shù)組 和 普通對象 在使用場景下有區(qū)別,在項目中使用數(shù)組的目的大多是為了 遍歷,即比較少會使用 array[index] = xxx 的形式,更多的是使用數(shù)組的 Api 的方式
          • 數(shù)組長度是多變的,不可能像普通對象一樣先在 data 選項中提前聲明好所有元素,比如通過 array[index] = xxx 方式賦值時,一旦 index 的值超過了現(xiàn)有的最大索引值,那么當前的添加的新元素也不會具有響應(yīng)式
          • 數(shù)組存儲的元素比較多,不可能為每個數(shù)組元素都設(shè)置 getter/setter
          • 無法攔截數(shù)組原生方法如 push、pop、shift、unshift 等的調(diào)用,最終仍需 重寫/增強 原生方法

          Proxy & Reflect

          由于在 Vue2 中使用 Object.defineProperty 帶來的缺陷,導(dǎo)致在 Vue2 中不得不提供了一些額外的方法(如:Vue.set、Vue.delete())解決問題,而在 Vue3 中使用了 Proxy 的方式來實現(xiàn) 數(shù)據(jù)劫持,而上述的問題在 Proxy 中都可以得到解決。

          Proxy

          **`Proxy`**[6] 主要用于創(chuàng)建一個 對象的代理,從而實現(xiàn)基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等),本質(zhì)上是通過攔截對象 內(nèi)部方法 的執(zhí)行實現(xiàn)代理,而對象本身根據(jù)規(guī)范定義的不同又會區(qū)分為 常規(guī)對象異質(zhì)對象(這不是重點,可自行了解)。

          • new Proxy(target, handler) 是針對整個對象進行的代理,不是某個屬性
          • 代理對象屬性擁有 讀取、修改、刪除、新增、是否存在屬性 等操作相應(yīng)的捕捉器,**更多可見**[7]
            • `get()`[8] 屬性 讀取 操作的捕捉器
            • `set()`[9] 屬性 設(shè)置 操作的捕捉器
            • `deleteProperty()`[10]`delete`[11] 操作符的捕捉器
            • `ownKeys()`[12]`Object.getOwnPropertyNames`[13] 方法和 `Object.getOwnPropertySymbols`[14] 方法的捕捉器
            • `has()`[15]`in`[16] 操作符的捕捉器

          Reflect

          **`Reflect`**[17] 是一個內(nèi)置的對象,它提供攔截 JavaScript 操作的方法,這些方法與 **`Proxy handlers`**[18] 提供的的方法是一一對應(yīng)的,且 Reflect 不是一個函數(shù)對象,即不能進行實例化,其所有屬性和方法都是靜態(tài)的。

          • `Reflect.get(target, propertyKey[, receiver])`[19] 獲取對象身上某個屬性的值,類似于 target[name]
          • `Reflect.set(target, propertyKey, value[, receiver])`[20] 將值分配給屬性的函數(shù)。返回一個`Boolean`[21],如果更新成功,則返回true
          • `Reflect.deleteProperty(target, propertyKey)`[22] 作為函數(shù)的`delete`[23]操作符,相當于執(zhí)行 delete target[name]
          • `Reflect.ownKeys(target)`[24] 返回一個包含所有自身屬性(不包含繼承屬性)的數(shù)組。(類似于 `Object.keys()`[25], 但不會受enumerable 影響)
          • `Reflect.has(target, propertyKey)`[26] 判斷一個對象是否存在某個屬性,和 `in` 運算符[27] 的功能完全相同

          **更多方法點此可見**[28]

          Proxy 為什么需要 Reflect 呢?

          Proxyget(target, key, receiver)、set(target, key, newVal, receiver) 的捕獲器中都能接到前面所列舉的參數(shù):

          • target 指的是 原始數(shù)據(jù)對象
          • key 指的是當前操作的 屬性名
          • newVal 指的是當前操作接收到的 最新值
          • receiver 指向的是當前操作 正確的上下文

          怎么理解 Proxy handlerreceiver 指向的是當前操作正確上的下文呢?

          • 正常情況下,**receiver** 指向的是 當前的代理對象

            image.png
          • 特殊情況下,**receiver** 指向的是 引發(fā)當前操作的對象

            image.png
            • 通過 Object.setPrototypeOf() 方法將代理對象 proxy 設(shè)置為普通對象 obj 的原型
            • 通過 obj.name 訪問其不存在的 name 屬性,由于原型鏈的存在,最終會訪問到 proxy.name 上,即觸發(fā) get 捕獲器

          Reflect 的方法中通常只需要傳遞 target、key、newVal 等,但為了能夠處理上述提到的特殊情況,一般也需要傳遞 receiver 參數(shù),因為 Reflect 方法中傳遞的 receiver 參數(shù)代表執(zhí)行原始操作時的 this 指向,比如:Reflect.get(target, key , receiver)、Reflect.set(target, key, newVal, receiver)。

          總結(jié):**Reflect** 是為了在執(zhí)行對應(yīng)的攔截操作的方法時能 傳遞正確的 this 上下文。

          Vue3 如何使用 Proxy 實現(xiàn)數(shù)據(jù)劫持?

          Vue3 中提供了 reactive()ref() 兩個方法用來將 目標數(shù)據(jù) 變成 響應(yīng)式數(shù)據(jù),而通過 Proxy 來實現(xiàn) 數(shù)據(jù)劫持(或代理) 的具體實現(xiàn)就在其中,下面一起來看看吧!

          2528D3FF.png

          reactive 函數(shù)

          從源碼來看,其核心其實就是 createReactiveObject(...) 函數(shù),那么繼續(xù)往下查看對應(yīng)的內(nèi)容

          源碼位置:packages\reactivity\src\reactive.ts

          export function reactive(target: object{
            // if trying to observe a readonly proxy, return the readonly version.
            // 若目標對象是響應(yīng)式的只讀數(shù)據(jù),則直接返回
            if (isReadonly(target)) {
              return target
            }

            // 否則將目標數(shù)據(jù)嘗試變成響應(yīng)式數(shù)據(jù)
            return createReactiveObject(
              target,
              false,
              mutableHandlers, // 對象類型的 handlers
              mutableCollectionHandlers, // 集合類型的 handlers
              reactiveMap
            )
          }
          復(fù)制代碼

          createReactiveObject() 函數(shù)

          源碼的體現(xiàn)也是非常簡單,無非就是做一些前置判斷處理:

          • 若目標數(shù)據(jù)是 原始值類型,直接向返回 原數(shù)據(jù)
          • 若目標數(shù)據(jù)的 __v_raw 屬性為 true,且是【非響應(yīng)式數(shù)據(jù)】或 不是通過調(diào)用 readonly() 方法,則直接返回 原數(shù)據(jù)
          • 若目標數(shù)據(jù)已存在相應(yīng)的 proxy 代理對象,則直接返回 對應(yīng)的代理對象
          • 若目標數(shù)據(jù)不存在對應(yīng)的 白名單數(shù)據(jù)類型 中,則直接返回原數(shù)據(jù),支持響應(yīng)式的數(shù)據(jù)類型如下:
            • 可擴展的對象,即是否可以在它上面添加新的屬性
            • __v_skip 屬性不存在或值為 false 的對象
            • 數(shù)據(jù)類型為 Object、Array、Map、Set、WeakMap、WeakSet 的對象
            • 其他數(shù)據(jù)都統(tǒng)一被認為是 無效的響應(yīng)式數(shù)據(jù)對象
          • 通過 Proxy 創(chuàng)建代理對象,根據(jù)目標數(shù)據(jù)類型選擇不同的 Proxy handlers

          看來具體的實現(xiàn)又在不同數(shù)據(jù)類型的 捕獲器 中,即下面源碼的 collectionHandlersbaseHandlers ,而它們則對應(yīng)的是在上述 reactive() 函數(shù)中為 createReactiveObject() 函數(shù)傳遞的 mutableCollectionHandlersmutableHandlers 參數(shù)。

          源碼位置:packages\reactivity\src\reactive.ts

          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
            }

            // 目標數(shù)據(jù)的 __v_raw 屬性若為 true,且是【非響應(yīng)式數(shù)據(jù)】或 不是通過調(diào)用 readonly() 方法,則直接返回
            if (
              target[ReactiveFlags.RAW] &&
              !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
            ) {
              return target
            }

            // 目標對象已存在相應(yīng)的 proxy 代理對象,則直接返回
            const existingProxy = proxyMap.get(target)
            if (existingProxy) {
              return existingProxy
            }

            // 只有在白名單中的值類型才可以被代理監(jiān)測,否則直接返回
            const targetType = getTargetType(target)
            if (targetType === TargetType.INVALID) {
              return target     
            }

            // 創(chuàng)建代理對象
            const proxy = new Proxy(
              target,
              // 若目標對象是集合類型(Set、Map)則使用集合類型對應(yīng)的捕獲器,否則使用基礎(chǔ)捕獲器
              targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers 
            )

            // 將對應(yīng)的代理對象存儲在 proxyMap 中
            proxyMap.set(target, proxy)

            return proxy
          }
          復(fù)制代碼

          捕獲器 Handlers

          對象類型的捕獲器 — mutableHandlers

          這里的對象類型指的是 數(shù)組普通對象

          源碼位置:packages\reactivity\src\baseHandlers.ts

          export const mutableHandlers: ProxyHandler<object> = {
            get,
            set,
            deleteProperty,
            has,
            ownKeys
          }
          復(fù)制代碼

          以上這些捕獲器其實就是我們在上述 Proxy 部分列舉出來的捕獲器,顯然可以攔截對普通對象的如下操作:

          • 讀取,如 obj.name
          • 設(shè)置,如 obj.name = 'zs'
          • 刪除屬性,如 delete obj.name
          • 判斷是否存在對應(yīng)屬性,如 name in obj
          • 獲取對象自身的屬性值,如 obj.getOwnPropertyNames()obj.getOwnPropertySymbols()

          get 捕獲器

          具體信息在下面的注釋中,這里只列舉核心內(nèi)容:

          • 若當前數(shù)據(jù)對象是 數(shù)組,則 重寫/增強 數(shù)組對應(yīng)的方法
          • 數(shù)組元素的 查找方法includes、indexOf、lastIndexOf
          • 修改原數(shù)組 的方法:push、pop、unshift、shift、splice
          • 若當前數(shù)據(jù)對象是 普通對象,且非 只讀 的則通過 track(target, TrackOpTypes.GET, key) 進行 依賴收集
          • 若當前數(shù)據(jù)對象是 淺層響應(yīng) 的,則直接返回其對應(yīng)屬性值
          • 若當前數(shù)據(jù)對象是 ref 類型的,則會進行 自動脫 ref
          • 若當前數(shù)據(jù)對象的屬性值是 對象類型
          • 若當前屬性值屬于 只讀的,則通過 readonly(res) 向外返回其結(jié)果
          • 否則會將當前屬性值以 reactive(res) 向外返回 proxy 代理對象
          • 否則直接向外返回對應(yīng)的 屬性值
          function createGetter(isReadonly = false, shallow = false{
            return function get(target: Target, key: string | symbol, receiver: object{
              // 當直接通過指定 key 訪問 vue 內(nèi)置自定義的對象屬性時,返回其對應(yīng)的值
              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
              }

              // 判斷是否為數(shù)組類型
              const targetIsArray = isArray(target)

              // 數(shù)組對象
              if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
                // 重寫/增強數(shù)組的方法: 
                //  - 查找方法:includes、indexOf、lastIndexOf
                //  - 修改原數(shù)組的方法:push、pop、unshift、shift、splice
                return Reflect.get(arrayInstrumentations, key, receiver)
              }

              // 獲取對應(yīng)屬性值
              const res = Reflect.get(target, key, receiver)

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

              // 依賴收集
              if (!isReadonly) {
                track(target, TrackOpTypes.GET, key)
              }

              // 淺層響應(yīng)
              if (shallow) {
                return res
              }

              // 若是 ref 類型響應(yīng)式數(shù)據(jù),會進行【自動脫 ref】,但不支持【數(shù)組】+【索引】的訪問方式
              if (isRef(res)) {
                const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
                return shouldUnwrap ? res.value : res
              }

              // 屬性值是對象類型:
              //  - 是只讀屬性,則通過 readonly() 返回結(jié)果,
              //  - 且是非只讀屬性,則遞歸調(diào)用 reactive 向外返回 proxy 代理對象
              if (isObject(res)) {
                return isReadonly ? readonly(res) : reactive(res)
              }

              return res
            }
          }
          復(fù)制代碼

          set 捕獲器

          除去額外的邊界處理,其實核心還是 更新屬性值,并通過 trigger(...) 觸發(fā)依賴更新

          function createSetter(shallow = false{
            return function set(
              target: object,
              key: string | symbol,
              value: unknown,
              receiver: object
            
          ): boolean 
          {
              // 保存舊的數(shù)據(jù)
              let oldValue = (target as any)[key]

              // 若原數(shù)據(jù)值屬于 只讀 且 ref 類型,并且新數(shù)據(jù)值不屬于 ref 類型,則意味著修改失敗
              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
              }

              // 是否存在對應(yīng)的 key
              const hadKey =
                isArray(target) && isIntegerKey(key)
                  ? Number(key) < target.length
                  : hasOwn(target, key)

              // 設(shè)置對應(yīng)值
              const result = Reflect.set(target, key, value, receiver)

              // 若目標對象是原始原型鏈上的內(nèi)容(非自定義添加),則不觸發(fā)依賴更新
              if (target === toRaw(receiver)) {
                if (!hadKey) {
                  // 目標對象不存在對應(yīng)的 key,則為新增操作
                  trigger(target, TriggerOpTypes.ADD, key, value)
                } else if (hasChanged(value, oldValue)) {
                  // 目標對象存在對應(yīng)的值,則為修改操作
                  trigger(target, TriggerOpTypes.SET, key, value, oldValue)
                }
              }

              // 返回修改結(jié)果
              return result
            }
          }
          復(fù)制代碼

          deleteProperty & has & ownKeys 捕獲器

          這三個捕獲器內(nèi)容非常簡潔,其中 hasownKeys 本質(zhì)也屬于 讀取操作,因此需要通過 track() 進行依賴收集,而 deleteProperty 相當于修改操作,因此需要 trigger() 觸發(fā)更新

          function deleteProperty(target: object, key: string | symbol): boolean {
            const hadKey = hasOwn(target, key)
            const oldValue = (target as any)[key]
            const result = Reflect.deleteProperty(target, key)
            // 目標對象上存在對應(yīng)的 key ,并且能成功刪除,才會觸發(fā)依賴更新
            if (result && hadKey) {
              trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
            }
            return result
          }

          function has(target: object, key: string | symbol): boolean {
            const result = Reflect.has(target, key)
            if (!isSymbol(key) || !builtInSymbols.has(key)) {
              track(target, TrackOpTypes.HAS, key)
            }
            return result
          }

          function ownKeys(target: object): (string | symbol)[] {
            track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
            return Reflect.ownKeys(target)
          }
          復(fù)制代碼

          數(shù)組類型捕獲器 —— arrayInstrumentations

          數(shù)組類型對象類型 的大部分操作是可以共用的,比如 obj.namearr[index] 等,但數(shù)組類型的操作還是會比對象類型更豐富一些,而這些就需要特殊處理。

          源碼位置:packages\reactivity\src\collectionHandlers.ts

          處理數(shù)組索引 indexlength

          數(shù)組的 indexlength 是會相互影響的,比如存在數(shù)組 const arr = [1]

          • arr[1] = 2 的操作會隱式修改 length 的屬性值
          • arr.length = 0 的操作會導(dǎo)致原索引位的值發(fā)生變更

          為了能夠合理觸發(fā)和 length 相關(guān)副作用函數(shù)的執(zhí)行,在 set() 捕獲器中會判斷當前操作的類型:

          • Number(key) < target.length 證明是修改操作,對應(yīng) TriggerOpTypes.SET 類型,即當前操作不會改變 length 的值,不需要 觸發(fā)和 length 相關(guān)副作用函數(shù)的執(zhí)行
          • Number(key) >= target.length 證明是新增操作,TriggerOpTypes.ADD 類型,即當前操作會改變 length 的值,需要 觸發(fā)和 length 相關(guān)副作用函數(shù)的執(zhí)行
          function createSetter(shallow = false{
            return function set(
              target: object,
              key: string | symbol,
              value: unknown,
              receiver: object
            
          ): boolean 
          {
            
             省略其他代碼
             
              const hadKey =
                isArray(target) && isIntegerKey(key)
                  ? Number(key) < target.length
                  : hasOwn(target, key)
                  
              const result = Reflect.set(target, key, value, receiver)
              // don't trigger if target is something up in the prototype chain of original
              
              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
            }
          }
          復(fù)制代碼

          處理數(shù)組的查找方法

          數(shù)組的查找方法包括 includes、indexOflastIndexOf,這些方法通常情況下是能夠按預(yù)期進行工作,但還是需要對某些特殊情況進行處理:

          • 當查找的目標數(shù)據(jù)是響應(yīng)式數(shù)據(jù)本身時,得到的就不是預(yù)期結(jié)果

            const obj = {}
            const proxy = reactive([obj])
            console.log(proxy.includs(proxy[0])) // false
            復(fù)制代碼
            • 產(chǎn)生原因】首先這里涉及到了兩次讀取操作,第一次proxy[0] 此時會觸發(fā) get 捕獲器并為 obj 生成對應(yīng)代理對象并返回,第二次proxy.includs() 的調(diào)用,它會遍歷數(shù)組的每個元素,即會觸發(fā) get 捕獲器,并又生成一個新的代理對象并返回,而這兩次生成的代理對象不是同一個,因此返回 false
            • 解決方案】源碼中會在 get 中設(shè)置一個名為 proxyMapWeakMap 集合用于存儲每個響應(yīng)式對象,在觸發(fā) get 時優(yōu)先返回 proxyMap 存在的響應(yīng)式對象,這樣不管觸發(fā)多少次 get 都能返回相同的響應(yīng)式數(shù)據(jù)
          • 當在響應(yīng)式對象中查找原始數(shù)據(jù)時,得到的就不是預(yù)期結(jié)果

            const obj = {}
            const proxy = reactive([obj])
            console.log(proxy.includs(obj)) // false
            復(fù)制代碼
            • 在 重寫/增強 的 includes、indexOf、lastIndexOf 等方法中,會將當前方法內(nèi)部訪問到的響應(yīng)式數(shù)據(jù)轉(zhuǎn)換為原始數(shù)據(jù),然后調(diào)用數(shù)組對應(yīng)的原始方法進行查找,若查找結(jié)果為 true 則直接返回結(jié)果
            • 若以上操作沒有查找到,則通過將當前方法傳入的參數(shù)轉(zhuǎn)換為原始數(shù)據(jù),在調(diào)用數(shù)組的原始方法,此時直接將對應(yīng)的結(jié)果向外進行返回
            • 產(chǎn)生原因proxy.includes() 會觸發(fā) get 捕獲器并為 obj 生成對應(yīng)代理對象并返回,而 includes 方法的參數(shù)傳遞的是 原始數(shù)據(jù),相當于此時是 響應(yīng)式對象原始數(shù)據(jù)對象 進行比較,因此對應(yīng)的結(jié)果一定是為 false
            • 解決方案】核心就是將它們的數(shù)據(jù)類型統(tǒng)一,即統(tǒng)一都使用 原始值數(shù)據(jù)對比響應(yīng)式數(shù)據(jù)對比,由于 includes() 的方法本身并不支持對傳入?yún)?shù)或內(nèi)部響應(yīng)式數(shù)據(jù)的處理,因此需要自定義以上對應(yīng)的數(shù)組查找方法

          源碼位置:packages\reactivity\src\baseHandlers.ts

          ;(['includes''indexOf''lastIndexOf'as const).forEach(key => {
              instrumentations[key] = function (this: unknown[], ...args: unknown[]{
                // 外部調(diào)用上述方法,默認其內(nèi)的 this 指向的是代理數(shù)組對象,
                // 但實際上是需要通過原始數(shù)組中進行遍歷查找
                const arr = toRaw(thisas any
                for (let i = 0, l = this.length; i < l; i++) {
                  track(arr, TrackOpTypes.GET, i + '')
                }
                // we run the method using the original args first (which may be reactive)
                const res = arr[key](...args)
                if (res === -1 || res === false) {
                  // if that didn't work, run it again using raw values.
                  return arr[key](...args.map(toRaw))
                } else {
                  return res
                }
              }
            })
          復(fù)制代碼

          處理數(shù)組影響 length 的方法

          隱式修改數(shù)組長度的原型方法包括 push、pop、shiftunshift、splice 等,在調(diào)用這些方法的同時會間接的讀取數(shù)組的 length 屬性,又因為這些方法具有修改數(shù)組長度的能力,即相當于 length 的設(shè)置操作,若不進行特殊處理,會導(dǎo)致與 length 屬性相關(guān)的副作用函數(shù)被重復(fù)執(zhí)行,即 棧溢出,比如:

          const proxy = reactive([])

          // 第一個副作用函數(shù)
          effect(() => {
            proxy.push(1// 讀取 + 設(shè)置 操作
          })

          // 第二個副作用函數(shù)
          effect(() => {
            proxy.push(2// 讀取 + 設(shè)置 操作(此時進行 trigger 時,會觸發(fā)包括第一個副作用函數(shù)的內(nèi)容,然后循環(huán)導(dǎo)致棧溢出)
          })
          復(fù)制代碼

          在源碼中還是通過 重寫/增強 上述對應(yīng)數(shù)組方法的形式實現(xiàn)自定義的邏輯處理:

          • 在調(diào)用真正的數(shù)組原型方法前,會通過設(shè)置 pauseTracking() 方法來禁止 track 依賴收集
          • 在調(diào)用數(shù)組原生方法后,在通過 resetTracking() 方法恢復(fù) track 進行依賴收集
          • 實際上以上的兩個方法就是通過控制 shouldTrack 變量為 truefalse,使得在 track 函數(shù)執(zhí)行時是否要執(zhí)行原來的依賴收集邏輯

          源碼位置:packages\reactivity\src\baseHandlers.ts

          ;(['push''pop''shift''unshift''splice'as const).forEach(key => {
              instrumentations[key] = function (this: unknown[], ...args: unknown[]{
                pauseTracking()
                const res = (toRaw(thisas any)[key].apply(this, args)
                resetTracking()
                return res
              }
            })
          復(fù)制代碼

          集合類型的捕獲器 — mutableCollectionHandlers

          集合類型 包括 Map、WeakMap、SetWeakSet 等,而對 集合類型代理模式對象類型 需要有所不同,因為 集合類型對象類型 的操作方法是不同的,比如:

          Map 類型 的原型 屬性方法 如下,**詳情可見**[29]

          • size
          • clear()
          • delete(key)
          • has(key)
          • get(key)
          • set(key)
          • keys()
          • values()
          • entries()
          • forEach(cb)

          Set 類型 的原型 屬性方法 如下,**詳情可見**[30]

          • size
          • add(value)
          • clear()
          • delete(value)
          • has(value)
          • keys()
          • values()
          • entries()
          • forEach(cb)

          源碼位置:packages\reactivity\src\collectionHandlers.ts

          解決 代理對象 無法訪問 集合類型 對應(yīng)的 屬性方法

          代理集合類型的第一個問題,就是代理對象沒法獲取到集合類型的屬性和方法,比如:

          從報錯信息可以看出 size 屬性是一個訪問器屬性,所以它被作為方法調(diào)用了,而主要錯誤原因就是在這個訪問器中的 this 指向的是 代理對象,在源碼中就是通過為這些特定的 屬性方法 定義對應(yīng)的 keymutableInstrumentations 對象,并且在其對應(yīng)的 屬性方法 中將 this指向為 原對象.

          function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
            const target = (this as any)[ReactiveFlags.RAW]
            const rawTarget = toRaw(target)
            const rawKey = toRaw(key)
            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)
          }

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

          省略其他代碼
          復(fù)制代碼

          處理集合類型的響應(yīng)式

          集合建立響應(yīng)式核心還是 tracktrigger,轉(zhuǎn)而思考的問題就變成,什么時候需要 track、什么時候需要 trigger:

          • track 時機:get()、get size()、has()、forEach()
          • trigger 時機:add()、set()、delete()、clear()

          這里涉及一些優(yōu)化的內(nèi)容,比如:

          • add() 中通過 has() 判斷當前添加的元素是否已經(jīng)存在于 Set 集合中時,若已存在就不需要進行 trigger() 操作,因為 Set 集合本身的一個特性就是 去重
          • delete() 中通過 has() 判斷當前刪除的元素或?qū)傩允欠翊嬖?,若不存在就不需要進行 trigger() 操作,因為此時的刪除操作是 無效的
          function createInstrumentations() {
            const mutableInstrumentations: Record<string, Function> = {
              get(this: MapTypes, key: unknown) {// track
                return get(this, key)
              },
              get size() {// track
                return size(this as unknown as IterableCollections)
              },
              has,// track
              add,// trigger
              set,// trigger
              delete: deleteEntry,// trigger
              clear,// trigger
              forEach: createForEach(false, false) // track
            }
            省略其他代碼
          }
          復(fù)制代碼

          避免污染原始數(shù)據(jù)

          通過重寫集合類型的方法并手動指定其中的 this 指向為 原始對象 的方式,解決 代理對象 無法訪問 集合類型 對應(yīng)的 屬性方法 的問題,但這樣的實現(xiàn)方式也帶來了另一個問題:**原始數(shù)據(jù)被污染** 。

          簡單來說,我們只希望 代理對象(響應(yīng)式對象 才具備 依賴收集(track)依賴更新(trigger) 的能力,而通過 原始數(shù)據(jù) 進行的操作不應(yīng)該具有響應(yīng)式的能力。

          如果只是單純的把所有操作直接作用到 原始對象 上就不能保證這個結(jié)果,比如:

          0021E3A8.gif
           // 原數(shù)數(shù)據(jù) originalData1
            const originalData1 = new Map({});
            // 代理對象 proxyData1
            const proxyData1 = reactive(originalData1);

            // 另一個代理對象 proxyData2
            const proxyData2 = reactive(new Map({}));

            // 將 proxyData2 做為 proxyData1 一個鍵值
            // 【注意】此時的 set() 經(jīng)過重寫,其內(nèi)部 this 已經(jīng)指向 原始對象(originalData1),等價于 原始對象 originalData1 上存儲了一個 響應(yīng)式對象 proxyData2
            proxyData1.set("proxyData2", proxyData2);

            // 若不做額外處理,如下基于 原始數(shù)據(jù)的操作 就會觸發(fā) track 和 trigger
            originalData1.get("proxyData2").set("name""zs");
          復(fù)制代碼

          在源碼中的解決方案也是很簡單,直接通過 value = toRaw(value) 獲取當前設(shè)置值對應(yīng)的 原始數(shù)據(jù),這樣舊可以避免 響應(yīng)式數(shù)據(jù)對原始數(shù)據(jù)的污染。

          處理 forEach 回調(diào)參數(shù)

          首先 `Map.prototype.forEach(callbackFn [, thisArg])`[31] 其中 callbackFn 回調(diào)函數(shù)會接收三個參數(shù):

          • 當前的 value
          • 當前的 key
          • 正在被遍歷的 Map 對象(原始對象)

          遍歷操作 等價于 讀取操作,在處理 普通對象get() 捕獲器中有一個處理,如果當前訪問的屬性值是 對象類型 那么就會向外返回其對應(yīng)的 代理對象,目的是實現(xiàn) 惰性響應(yīng)深層響應(yīng),這個處理也同樣適用于 集合類型。

          因此,在源碼中通過 callback.call(thisArg, wrap(value), wrap(key), observed) 的方式將 Map 類型的 進行響應(yīng)式處理,以及進行 track 操作,因為 Map 類型關(guān)注的就是

          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) => {
                // important: make sure the callback is
                // 1. invoked with the reactive map as `this` and 3rd arg
                // 2. the value received should be a corresponding reactive/readonly.
                return callback.call(thisArg, wrap(value), wrap(key), observed)
              })
            }
          復(fù)制代碼

          處理迭代器

          集合類型的迭代器方法:

          • entries()
          • keys()
          • values()

          MapSet 都實現(xiàn)了 可迭代協(xié)議(即 Symbol.iterator 方法,而 迭代器協(xié)議 是指 一個對象實現(xiàn)了 next 方法),因此它們還可以通過 for...of 的方式進行遍歷。

          根據(jù)對 forEach 的處理,不難知道涉及遍歷的方法,終究還是得將其對應(yīng)的遍歷的 鍵、值 進行響應(yīng)式包裹的處理,以及進行 track 操作,而原本的的迭代器方法沒辦法實現(xiàn),因此需要內(nèi)部自定義迭代器協(xié)議。

          const iteratorMethods = ['keys''values''entries'Symbol.iterator]
            iteratorMethods.forEach(method => {
              mutableInstrumentations[method as string] = createIterableMethod(
                method,
                false,
                false
              )
              省略其他代碼
            })
          復(fù)制代碼

          這一部分的源碼涉及的內(nèi)容比較多,以上只是簡單的總結(jié)一下,更詳細的內(nèi)容可查看對應(yīng)的源碼內(nèi)容。

          ref 函數(shù) — 原始值的響應(yīng)式

          原始值指的是 Boolean、Number、BigInt、String、Symbol、undefined、null 等類型的值,我們知道用 Object.defineProperty 肯定是不支持,因為它攔截的就是對象屬性的操作,都說 ProxyObject.defineProperty 強,那么它能不能直接支持呢?

          直接支持是肯定不能的,別忘了 Proxy 代理的目標也還是對象類型呀,它的強是在自己的所屬領(lǐng)域,跨領(lǐng)域也是遭不住的。

          036479D5.png

          因此在 Vue3ref 函數(shù)中對原始值的處理方式是通過為 原始值類型 提供一個通過 new RefImpl(rawValue, shallow) 實例化得到的 包裹對象,說白了還是將原始值類型變成對象類型,但 ref 函數(shù)的參數(shù)并 不限制數(shù)據(jù)類型

          • 原始值類型ref 函數(shù)中會為原始值類型數(shù)據(jù)創(chuàng)建 RefImpl 實例對象(必須通過 .value 的方式訪問數(shù)據(jù)),并且實現(xiàn)自定義的 get、set 用于分別進行 依賴收集依賴更新,注意的是這里并不會通過 Proxy 為原始值類型創(chuàng)建代理對象,準確的說在 RefImpl 內(nèi)部自定義實現(xiàn)的 get、set 就實現(xiàn)了對原始值類型的攔截操作,**因為原始值類型不需要向?qū)ο箢愋驮O(shè)置那么多的捕獲器**
          • 對象類型,ref 函數(shù)中除了為 對象類型 數(shù)據(jù)創(chuàng)建 RefImpl 實例對象之外,還會通過 reactive 函數(shù)將其轉(zhuǎn)換為響應(yīng)式數(shù)據(jù),其實主要還是為了支持類似如下的操作
            const refProxy = ref({name'zs'})
            refProxy.value.name = 'ls'
            復(fù)制代碼
          • 依賴容器 dep,在 ref 類型中依賴存儲的位置就是每個 ref 實例對象上的 dep 屬性,它本質(zhì)就是一個 Set 實例,觸發(fā) get 時往 dep 中添加副作用函數(shù)(依賴),觸發(fā) set 時從 dep 中依次取出副作用函數(shù)執(zhí)行

          源碼位置:packages\reactivity\src\ref.ts

          export function ref(value?: unknown{
            return createRef(value, false)
          }

          function createRef(rawValue: unknown, shallow: boolean{
            if (isRef(rawValue)) {
              return rawValue
            }
            return new RefImpl(rawValue, shallow)
          }

          class RefImpl<T{
            private _value: T
            private _rawValue: T

            public dep?: Dep = undefined
            public readonly __v_isRef = true

            constructor(value: T, public readonly __v_isShallow: boolean) {
              this._rawValue = __v_isShallow ? value : toRaw(value)
              this._value = __v_isShallow ? value : toReactive(value)
            }

            get value() {
              // 將依賴收集到 dep 中,實際上就是一個 Set 類型
              trackRefValue(this)
              return this._value
            }

            set value(newVal) {
              // 獲取原始數(shù)據(jù)
              newVal = this.__v_isShallow ? newVal : toRaw(newVal)

              // 通過 Object.is(value, oldValue) 判斷新舊值是否一致,若不一致才需要進行更新
              if (hasChanged(newVal, this._rawValue)) {
                // 保存原始值
                this._rawValue = newVal
                // 更新為新的 value 值
                this._value = this.__v_isShallow ? newVal : toReactive(newVal)
                // 依賴更新,從 dep 中取出對應(yīng)的 effect 函數(shù)依次遍歷執(zhí)行
                triggerRefValue(this, newVal)
              }
            }
          }

          // 若當前 value 是 對象類型,才會通過 reactive 轉(zhuǎn)換為響應(yīng)式數(shù)據(jù)
          export const toReactive = <T extends unknown>(value: T): T =>
            isObject(value) ? reactive(value) : value
          復(fù)制代碼

          Vue3 如何進行依賴收集?

          Vue2 中依賴的收集方式是通過 DepWatcher觀察者模式 來實現(xiàn)的,是不是還能想起初次了解 DepWatcher 之間的這種 剪不斷理還亂 的關(guān)系時的心情 ......

          關(guān)于 設(shè)計模式 部分感興趣可查看 **常見 JavaScript 設(shè)計模式 — 原來這么簡單**[32] 一文,里面主要圍繞著 Vue 中對應(yīng)的設(shè)計模式來進行介紹,相信會有一定的幫助

          依賴收集 其實說的就是 track 函數(shù)需要處理的內(nèi)容:

          • 聲明 targetMap 作為一個容器,用于保存和當前響應(yīng)式對象相關(guān)的依賴內(nèi)容,本身是一個 WeakMap 類型
            • 選擇 WeakMap 類型作為容器,是因為 WeakMap(對象類型)的引用是 弱類型 的,一旦外部沒有對該 (對象類型)保持引用時,WeakMap 就會自動將其刪除,即 能夠保證該對象能夠正常被垃圾回收
            • Map 類型對 的引用則是 強引用 ,即便外部沒有對該對象保持引用,但至少還存在 Map 本身對該對象的引用關(guān)系,因此會導(dǎo)致該對象不能及時的被垃圾回收
          • 將對應(yīng)的 響應(yīng)式數(shù)據(jù)對象 作為 targetMap,存儲和當前響應(yīng)式數(shù)據(jù)對象相關(guān)的依賴關(guān)系 depsMap(屬于 Map 實例),即 depsMap 存儲的就是和當前響應(yīng)式對象的每一個 key 對應(yīng)的具體依賴
          • deps(屬于 Set 實例)作為 depsMap 每個 key 對應(yīng)的依賴集合,因為每個響應(yīng)式數(shù)據(jù)可能在多個副作用函數(shù)中被使用,并且 Set 類型用于自動去重的能力

          可視化結(jié)構(gòu)如下:

          源碼位置:packages\reactivity\src\effect.ts

          const targetMap = new WeakMap<any, KeyToDepMap>()

          export function track(target: object, type: TrackOpTypes, key: unknown{
            // 當前應(yīng)該進行依賴收集 且 有對應(yīng)的副作用函數(shù)時,才會進行依賴收集
            if (shouldTrack && activeEffect) {
              // 從容器中取出【對應(yīng)響應(yīng)式數(shù)據(jù)對象】的依賴關(guān)系
              let depsMap = targetMap.get(target)
              if (!depsMap) {
                // 若不存在,則進行初始化
                targetMap.set(target, (depsMap = new Map()))
              }

              // 獲取和對【應(yīng)響應(yīng)式數(shù)據(jù)對象 key】相匹配的依賴
              let dep = depsMap.get(key)
              if (!dep) {
                // 若不存在,則進行初始化 dep 為 Set 實例
                depsMap.set(key, (dep = createDep()))
              }

              const eventInfo = __DEV__
                ? { effect: activeEffect, target, type, key }
                : undefined

              // 往 dep 集合中添加 effect 依賴
              trackEffects(dep, eventInfo)
            }
          }

          export const createDep = (effects?: ReactiveEffect[]): Dep => {
            const dep = new Set<ReactiveEffect>(effects) as Dep
            dep.w = 0
            dep.n = 0
            return dep
          }
          復(fù)制代碼

          最后

          以上就是針對 Vue3 中對不同數(shù)據(jù)類型的處理的內(nèi)容,無論是 Vue2 還是 Vue3 響應(yīng)式的核心都是 數(shù)據(jù)劫持/代理、依賴收集、依賴更新,只不過由于實現(xiàn)數(shù)據(jù)劫持方式的差異從而導(dǎo)致具體實現(xiàn)的差異,在 Vue3 中值得注意的是:

          • 普通對象類型 可以直接配合 Proxy 提供的捕獲器實現(xiàn)響應(yīng)式
          • 數(shù)組類型 也可以直接復(fù)用大部分和 普通對象類型 的捕獲器,但其對應(yīng)的查找方法和隱式修改 length 的方法仍然需要被 重寫/增強
          • 為了支持 集合類型 的響應(yīng)式,也對其對應(yīng)的方法進行了 重寫/增強
          • 原始值數(shù)據(jù)類型 主要通過 ref 函數(shù)來進行響應(yīng)式處理,不過內(nèi)容不會對 原始值類型 使用 reactive(或 Proxy) 函數(shù)來處理,而是在內(nèi)部自定義 get value(){}set value(){} 的方式實現(xiàn)響應(yīng)式,畢竟原始值類型的操作無非就是 讀取設(shè)置,核心還是將 原始值類型 轉(zhuǎn)變?yōu)榱?普通對象類型
            • ref 函數(shù)可實現(xiàn)原始值類型轉(zhuǎn)換為 響應(yīng)式數(shù)據(jù),但 ref 接收的值類型并沒只限定為原始值類型,若接收到的是引用類型,還是會將其通過 reactive 函數(shù)的方式轉(zhuǎn)換為響應(yīng)式數(shù)據(jù)

          肝了近 1W 字的內(nèi)容,也是目前寫得字數(shù)最多的文章,人有點麻了 ... 哈哈,希望對大家有所幫助?。?!

          05C751FE.jpg

          關(guān)于本文

          作者:熊的貓

          https://juejin.cn/post/7147461004954173471

          最后


          歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
          回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會很認真的解答喲!
          回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對你有幫助,在看」是最大的支持
           》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持

          瀏覽 88
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  爱爱视频免费看 | 操逼逼香蕉网 | 艹b视频| 国产夫妻精品 | 国产aa |