iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
Vue.js

從零到一打造 Vue3 響應式系統系列 第 27

Day 27 - toRef、toRefs、ProxyRef、unref

  • 分享至 

  • xImage
  •  

banner

響應式系統之中reactive 能夠將一個物件轉換為深層的響應式物件,但是在開發過程中我們時常會需要用到解構賦值,這時候會導致響應性遺失。

問題解析

<body>
  <div id="app"></div>
  <script type="module">
    import { reactive, toRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    // import { reactive, effect, ref } from '../dist/reactivity.esm.js'

    const state = reactive({
      name: 'a',
      age: 18
    })

    const { name } = state

    effect(() => {
      console.log(name)
    })

    setTimeout(() => {
      state.name = 'b'
    }, 1000)
  </script>
</body>

執行這段程式碼,你會發現解構出來的屬性會遺失響應式,所以 setTimeout 不會觸發更新。

day27-01

為了解決上述問題,我們通常會用 toRef ,讓解構出來的變數可以觸發響應式更新:

<body>
  <div id="app"></div>
  <script type="module">
    import { reactive, toRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    // import { reactive, effect, ref } from '../dist/reactivity.esm.js'

    const state = reactive({
      name: 'a',
      age: 18
    })

    const name = toRef(state, 'name')

    effect(() => {
      console.log(name.value)
    })

    setTimeout(() => {
      state.name = 'b'
    }, 1000)
  </script>
</body>

day27-02

核心原理

如果這時候去看這個 name 輸出的類型

day27-03

你會發現他跟我們在使用的 RefImpl 類型不同,它是一個特製的 ObjectRefImpl類別,並多了兩個屬性_object_key,它們分別儲存了原始物件、屬性名稱。

這個toRef我們可以知道他接受一個物件以及 key,所以我們可以這樣寫:

//ref.ts
export function toRef(target, key) {
  return{
    get value() {
      return target[key]
    },
    set value(newValue) {
      target[key] = newValue
    }
  }
}

這樣子其實就可以更新,但官方範例是屬於個類別,所以我們也改寫成類別:


class ObjectRefImpl {
  [ReactiveFlags.IS_REF] = true
  constructor(public _object, public key) {}

  get value() {
    return this._object[this.key]
  }

  set value(newValue) {
    this._object[this.key] = newValue
  }
}

export function toRef(target, key) {
  return new ObjectRefImpl(target, key)
}

這樣可以將我們解構出來的變數,重新賦予響應性。

toRefs

當需要處理多個屬性時,可以使用 toRefs,它會遍歷一個reactive 物件,並將其所有屬性都轉換為 ref,使用如下:

<body>
  <div id="app"></div>
  <script type="module">
    import { reactive, toRefs, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    // import { reactive, effect, toRef } from '../dist/reactivity.esm.js'

    const state = reactive({
      name: 'a',
      age: 18
    })
    const {name, age} = toRefs(state)

    effect(() => {
      console.log(age.value)
    })

    setTimeout(() => {
      state.age++
    }, 1000)
  </script>
</body>

day27-04

輸出age之後,可以看到它也是ObjectRefImpl類別。

day27-05

那我們可以知道toRefs 的實現非常直觀,它遍歷目標物件的所有 key,並為每一個 key 呼叫 toRef

export function toRefs(target) {
  const res = {}
  for (const key in target) {
    res[key] = new ObjectRefImpl(target, key)
  }
  return res
}

ps.toRefs原始碼中有另外寫判斷邏輯,確認傳入是不是響應式物件,這邊我們就省略判斷,讓它可以觸發更新:

day27-06

雖然 toRefs 解決了響應性遺失的問題,但到處都是 .value,所以我們這邊需要兩個輔助工具。

unref

unref 是一個簡單的輔助函式,如果參數是 ref,它返回 .value;如果不是,則直接返回參數本身 。

export function unref(value) {
  return isRef(value) ? value.value : value
}

ProxyRef

proxyRefs 可以將一個包含 ref 的物件(例如 toRefs 的回傳值)轉換為一個特殊的代理。當存取這個代理的屬性時,它會解包成ref。它跟 reactive 很像,不直接用 reactive 是因為 reactive 是深層物件,而 proxyRef 是淺層的物件。

export function proxyRefs(target) {
  return new Proxy(target, {
    get(...args) {
      const res = Reflect.get(...args)
      return unref(res)
    },
    set(target, key, newValue, receiver) {
      return Reflect.set(target, key, newValue, receiver)
    }
  })
}

這樣就完成了proxyRefs

今天我們重點在於:

  • 直接從 reactive 物件中解構,會失去響應性,所以可以使用 toRefs 將整個物件的所有屬性轉換成 ref,再進行解構。
  • 使用 toRefs 將整個物件的所有屬性轉換成 ref,再進行解構。這樣每個被解構出來的變數都與原始物件進行了響應式連結。
  • 選擇性地使用 proxyRefs 來建立一個自動解包的代理物件。

同步更新《嘿,日安!》技術部落格


上一篇
Day 26 - 陣列長度變更處理
下一篇
Day 28 - shallowRef、shallowReactive
系列文
從零到一打造 Vue3 響應式系統30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言