溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Vue3中響應(yīng)式的特殊處理方法是什么

發(fā)布時間:2023-04-11 15:19:27 來源:億速云 閱讀:83 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容介紹了“Vue3中響應(yīng)式的特殊處理方法是什么”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細閱讀,能夠?qū)W有所成!

vue2 vs vue3

兩個響應(yīng)式更新的核心區(qū)別在于Object.definePropertyProxy 兩個api 問題,經(jīng)過這兩個 api 能解決主要的響應(yīng)式問題。對于一些情況需要特殊處理

vue2 中不能實現(xiàn)的響應(yīng)式

  • arr.length

  • arr[0] = newVal

  • obj[newKey] = value

  • delete obj.key

對于這些情況 vue2 中通過增加Vue.$set和重寫數(shù)組方法來實現(xiàn)。然而對于 vue3 中,因為 proxy 是代理整個對象,所以它天生支持一個Object.defineProperty 不能支持的特性,比如他能偵聽到添加新屬性,而 Object.defineProperty因為代理的是每一個 key 所以它對于新增的屬性并不能知道。諸如此類,下面列出一些vue3 中不同的響應(yīng)式處理。

新增屬性的更新

proxy 雖然能夠監(jiān)聽到新屬性的添加,但新增的屬性并沒有經(jīng)過像已有屬性那樣的 getter 收集依賴,也就是并不能觸發(fā)更新。所以我們的目的變成如何收集響應(yīng)

首先,我們先看下 vue3 中是如何處理 for...in.. 循環(huán)的,可以知道的是循環(huán)的內(nèi)部使用了Reflect.ownKeys(obj) 來獲取只屬于對象自身擁有的鍵。所以對于 for..in 循環(huán)的攔截就可以清楚了

const obj = {foo: 1}
const ITERATE_KEY = symbol()
const p = new Proxy(obj, {
   track(target, ITERATE_KEY)
   return Reflect.ownKeys(target)
})

這里,我們用了一個symbol 的數(shù)據(jù)作為 收集依賴的key 因為這個是我們在遍歷中的攔截操作沒有與具體的 key 關(guān)聯(lián),而是一個整體性的攔截。在觸發(fā)響應(yīng)時,只要觸發(fā)這個 symbol 收集的 effect 就可以

trigger(target, isArray(target) ? 'length' : ITERATE_KEY) // 數(shù)組的情況追蹤 length

這里會發(fā)生影響遍歷對象長度時,會引ITERATE_KEY 相關(guān)的副作用函數(shù)執(zhí)行

effect(() => {
    for(let i in obj) {
        console.log(i)
    }
})

副作用函數(shù)執(zhí)行后,類比我們執(zhí)行了渲染函數(shù)。 然后回到我們的新增屬性,

p.newKey = 1

因為新增屬性,會對for.. in .. 循環(huán)產(chǎn)生影響,所以我們需要把與 ITERATE_KEY 相關(guān)的副作用函數(shù)拿出來重新執(zhí)行,看看源碼中這塊的處理

首先這里是 setter 的處理

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    ...
    // 這里是表明是否有 key 也就是判斷是否是新增元素
    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 邏輯
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

然后是 具體的 trigger,拿到對應(yīng)的標(biāo)識去更新effect

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }
    ...

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
    // 這種情況就是我們剛剛確定的 trigger 的執(zhí)行
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
        // 拿到收集的 ITERATE_KEY 的依賴
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          deps.push(depsMap.get('length'))
        }
        break
         ...
  }
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    ...
    triggerEffects(createDep(effects))
    ...
  }
}

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    if (effect.scheduler) {
      effect.scheduler()
    } else {
     // 執(zhí)行所有的 effect 函數(shù)
      effect.run()
    }
  }
}

總結(jié):

在遍歷對象或數(shù)組時,用一個唯一標(biāo)識symbol 收集依賴

  • 其實如果在模板中直接使用 obj 會伴隨著一個 JSON.stringify 的過程,也會伴隨著收集依賴。

  • 我們在 js 代碼里如果沒有用到遍歷對象,單獨對一個對象新增是不會觸發(fā)更新的,因為沒有收集的過程。

在設(shè)置新值時,獲取收集的symbol對應(yīng)的副作用函數(shù)更新

遍歷數(shù)組方法的處理

在使用數(shù)組時,會伴隨著 this 的問題導(dǎo)致代理對象拿不到屬性的問題,比如

const obj = {}
const arr = reactive([obj])
console.log(arr.includes(obj) // false

之所以會出現(xiàn)這樣的問題,是因為 includes 內(nèi)部的this 指向的是代理對象 arr, 并且在因為比較去獲取元素時拿到的也是代理對象,所以拿原始對象去找肯定找不到。所以,我們需要去修改inlcudes的行為,

new Proxy(obj, {
    get() {
        if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
          return Reflect.get(arrayInstrumentations, key, receiver)
        }
    }
})
   
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()

// 處理集中數(shù)組遍歷方法中的問題。
function createArrayInstrumentations() {
  const instrumentations: Record<string, Function> = {}
  // instrument identity-sensitive Array methods to account for possible reactive
  // values 
  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      const arr = toRaw(this) as 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)
      // 先使用原始參數(shù),可能是原始對象,也可能是代理對象。
      const res = arr[key](...args)
      if (res === -1 || res === false) {
        // if that didn't work, run it again using raw values.
        // 如果沒有找到,就拿到原始參數(shù)去比較,脫完響應(yīng)式的數(shù)據(jù)。
        return arr[key](...args.map(toRaw)) 
      } else {
        return res
      }
    }
  })

數(shù)組的變更方法

對于可能會更改原數(shù)組長度的數(shù)組方法,push, pop, shift, unshift, splice 也需要進行處理,否則,就會陷入無限遞歸當(dāng)中,考慮下面的場景

cosnt arr = reactive([])
effect(() => {
    arr.push(1)
})
effect(() => {
    arr.push(1)
})

這兩個的執(zhí)行過程如下:

  • 首先,第一個副作用函數(shù)執(zhí)行,然后數(shù)組中添加1, 并且這個過程會給影響數(shù)組的 length, 所以會與 length 會被 track , 建立響應(yīng)式聯(lián)系。

  • 然后第二個副作用函數(shù)執(zhí)行,執(zhí)行 push 這時因為影響了 length,先track 建立響應(yīng)式聯(lián)系,然后會試圖拿出length的副作用函數(shù)也就是第一個副作用函數(shù)執(zhí)行,然而這時第二個副作用函數(shù)還未執(zhí)行完成,就又開始執(zhí)行第一個副作用函數(shù)了,

  • 第一個副作用函數(shù)再次執(zhí)行,同樣會讀取length 并且設(shè)置 length,重復(fù)上面收集和更新的過程,又要把第二個副作用中收集的 length 執(zhí)行

  • 如此循環(huán)往復(fù)。最終會棧溢出。

所以問題的關(guān)鍵就在于 length 的不斷讀取和設(shè)置。所以我們需要在讀取到 length,避免它與副作用函數(shù)之間建立聯(lián)系

;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      // 禁止追蹤變化, 
      pauseTracking()
      const res = (toRaw(this) as any)[key].apply(this, args)
      // 等函數(shù)執(zhí)行完畢時再回復(fù)追蹤。
      resetTracking()
      return res
    }
  })

“Vue3中響應(yīng)式的特殊處理方法是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI