2024-09-12|閱讀時間 ‧ 約 22 分鐘

EP4 - 反應性基礎

Reactivity Fundamentals 這啥東西?越看文件頭越大www
每次學新東西都是先吞下去~隔一陣子再來看就會懂了
或者是要隔好幾年....www

在學習和使用 Vue.js 時,官方文件可切換API 風格。這樣的設計有助於開發者靈活地學習和應用不同的 API 風格,根據具體情況選擇最合適的方法來構建應用。
目前先專心研究Composition API,之後在來切換Option API來看差異~

聲明響應式狀態 - Declaring Reactive State

ref()

在 Composition API 中,聲明響應式狀態的推薦方式是使用 ref() 函數:

import { ref } from 'vue'

const count = ref(0)

ref() 接收一個參數並將其包裝在一個帶有 .value 屬性的 ref 對象中:

const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

參見: Typing Refs 

要在組件的模板中訪問 ref,需要在組件的 setup() 函數中聲明並返回它們:

import { ref } from 'vue'

export default {
// `setup` 是一個專門用於 Composition API 的特殊鉤子。
setup() {
const count = ref(0)

// 將 ref 暴露給模板
return {
count
}
}
}

模板:

<div>{{ count }}</div>

注意,在模板中使用 ref 時不需要附加 .value。為了方便起見,在模板中使用時,ref 會自動解包(有一些注意事項)。

你也可以在事件處理器中直接修改 ref

模板:

<button @click="count++">
{{ count }}
</button>

對於更複雜的邏輯,我們可以在同一範圍內聲明修改 ref 的函數,並將它們作為方法與狀態一起暴露出來:

import { ref } from 'vue'

export default {
setup() {
const count = ref(0)

function increment() {
// 在 JavaScript 中需要使用 .value
count.value++
}

// 別忘了也要暴露這個函數。
return {
count,
increment
}
}
}

暴露的方法可以作為事件處理器使用:

模板:

<button @click="increment">
{{ count }}
</button>

這裡有一個在 Codepen 上的實例,沒有使用任何構建工具。

<script setup>

使用 setup() 手動暴露狀態和方法可能會很繁瑣。幸運的是,當使用單文件組件(SFC)時,可以避免這種情況。我們可以使用 <script setup> 簡化:

<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
count.value++
}
</script>

<template>
<button @click="increment">
{{ count }}
</button>
</template>

在 Playground 中試試

<script setup> 中聲明的頂層導入、變量和函數可以自動在同一組件的模板中使用。可以將模板視為在相同作用域內聲明的 JavaScript 函數 - 它自然可以訪問與其一起聲明的所有內容。

提示:在文件其餘部分,將主要使用 SFC + <script setup> 語法來編寫 Composition API 的代碼示例,因為這是 Vue 開發者最常用的用法。如果您不使用 SFC,仍然可以使用 setup() 選項來使用 Composition API。

為什麼使用 Refs?

您可能會想知道為什麼我們需要具有 .value 的 refs,而不是普通變量。要解釋這一點,我們需要簡要討論 Vue 的響應系統是如何工作的。

當您在模板中使用 ref,並且稍後更改 ref 的值時,Vue 會自動檢測到變化並相應地更新 DOM。這是通過依賴關係追踪的響應系統實現的。當組件第一次渲染時,Vue 會追踪渲染過程中使用的每個 ref。稍後,當 ref 被修改時,它會觸發跟踪它的組件重新渲染。

在標準 JavaScript 中,無法檢測普通變量的訪問或修改。然而,我們可以通過 getter 和 setter 方法攔截對象屬性的 get 和 set 操作。

.value 屬性為 Vue 提供了檢測 ref 被訪問或修改的機會。在內部,Vue 在 getter 中執行追踪,在 setter 中執行觸發。概念上,可以將 ref 視為如下所示的對象:

// 假設代碼,非實際實現
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}

深度響應性 - Deep Reactivity

Refs 可以包含任何值類型,包括深層嵌套的對象、數組或 JavaScript 內建的數據結構如 Map。一個 ref 將使其值變得深度響應。這意味著即使您修改嵌套對象或數組,變化也會被檢測到:

import { ref } from 'vue'

const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})

function mutateDeeply() {
// 這些將按預期工作。
obj.value.nested.count++
obj.value.arr.push('baz')
}

非原始值通過 reactive() 變成響應代理,如下所述。

還可以通過淺層 refs 選擇不進行深度響應。對於淺層 refs,只有 .value 訪問會被追踪以實現響應。淺層 refs 可用於通過避免大對象的觀察成本來優化性能,或者在內部狀態由外部庫管理的情況下使用。

深入閱讀:

DOM 更新時間 - DOM Update Timing

當您修改響應狀態時,DOM 會自動更新。然而,應注意,DOM 更新不是同步應用的。相反,Vue 將它們緩衝到更新循環中的“下一個刻度”,以確保每個組件無論進行了多少狀態更改都只更新一次。

要在狀態更改後等待 DOM 更新完成,可以使用 nextTick() 全局 API:

import { nextTick } from 'vue'

async function increment() {
count.value++
await nextTick()
// 現在 DOM 已更新
}

reactive()

另一種聲明響應狀態的方法是使用 reactive() API。與 ref 將內部值包裝在特殊對象中不同,reactive() 使對象本身變得響應:

import { reactive } from 'vue'

const state = reactive({ count: 0 })

在模板中的使用方式:

<button @click="state.count++">
{{ state.count }}
</button>

響應對象是 JavaScript 的代理(Proxy),其行為與普通對象相同。不同之處在於,Vue 能夠攔截響應對象的所有屬性的訪問和修改,以進行響應性追蹤和觸發。

reactive() 會將對象進行深度轉換:嵌套對象在訪問時也會被 reactive() 包裝。當 ref 的值是一個對象時,它內部也會調用 reactive()。與淺層 refs 類似,也有 shallowReactive() API 可以選擇不進行深度響應。

響應代理 vs. 原始對象 (Reactive Proxy vs. Original)

需要注意的是,reactive() 返回的值是原始對象的代理,這與原始對象不相等:

const raw = {}
const proxy = reactive(raw)

// 代理與原始對象不相等
console.log(proxy === raw) // false

只有代理是響應的 - 修改原始對象不會觸發更新。因此,在使用 Vue 的響應系統時,最佳實踐是僅使用狀態的代理版本。

為了確保一致地訪問代理,對同一對象調用 reactive() 總是返回相同的代理,對已存在的代理調用 reactive() 也返回相同的代理:

// 對同一對象調用 reactive() 返回相同的代理
console.log(reactive(raw) === proxy) // true

// 對代理調用 reactive() 返回自身
console.log(reactive(proxy) === proxy) // true

這一規則也適用於嵌套對象。由於深度響應,響應對象內的嵌套對象也是代理:

const proxy = reactive({})

const raw = {}
proxy.nested = raw

console.log(proxy.nested === raw) // false

reactive() 的限制

reactive() API 有一些限制:

  1. 受限的值類型:它僅適用於對象類型(對象、數組和集合類型,如 Map 和 Set)。不能包含基本類型,如字符串、數字或布爾值。
  2. 不能替換整個對象:由於 Vue 的響應性追蹤是基於屬性訪問的,我們必須始終保持對響應對象的相同引用。這意味著我們不能輕易“替換”一個響應對象,因為對第一個引用的響應連接將丟失:
    let state = reactive({ count: 0 })

    // 上面的引用({ count: 0 })不再被追蹤
    // (響應連接丟失!)
    state = reactive({ count: 1 })
  3. 不適合解構:當我們將響應對象的基本類型屬性解構為本地變量,或將該屬性傳遞給函數時,我們會丟失響應連接:
const state = reactive({ count: 0 })

// 當解構時,count 與 state.count 斷開連接
let { count } = state
// 不影響原始狀態
count++

// 函數接收一個普通數字,無法追蹤 state.count 的變化
// 我們必須傳入整個對象以保留響應性
callSomeFunction(state.count)

由於這些限制,我們建議使用 ref() 作為聲明響應狀態的主要 API。ref 更靈活且更易於處理,特別是當涉及到基本類型和對象解構時。

refreactive 的比較

  1. 簡單值 vs. 複雜結構
    • ref 更適合用來處理簡單的基本類型值(如數字、字符串、布爾值)。
    • reactive 更適合用來處理包含多個屬性的複雜對象,因為它會自動對對象內部的所有屬性進行深度響應。
  2. 使用方式
    • ref 需要通過 .value 屬性來訪問和修改其包含的值,這在處理複雜對象時可能會變得繁瑣。
    • reactive 可以`直接通過屬性訪問和修改其內容,這使得代碼更簡潔和直觀。
  3. 可讀性和代碼風格
    • 當需要處理多個響應式屬性時,使用 reactive 可使代碼更具結構性和可讀性。
    • 對於單一的響應式屬性,使用 ref 更直觀。

為什麼需要 reactive

  1. 深度響應reactive 自動對其內部的所有屬性進行深度響應,這對於處理嵌套結構的對象非常方便。例如:
    import { reactive } from 'vue'

    const state = reactive({
    user: {
    name: 'John',
    age: 30
    },
    tasks: [
    { title: 'Task 1', completed: false },
    { title: 'Task 2', completed: true }
    ]
    })

    state.user.name = 'Jane' // 自動響應
    state.tasks.push({ title: 'Task 3', completed: false }) // 自動響應
  2. 更直觀的語法: 當處理多個屬性時,reactive 提供了更直觀的語法,無需頻繁使用 .value
import { reactive } from 'vue'

const state = reactive({
count: 0,
message: 'Hello'
})

state.count++
state.message = 'World'
  1. 一致性和風格偏好: 對於某些開發者來說,使用 reactive 使得狀態管理更具一致性,尤其在大型應用中,使用 reactive 可以提供更清晰的結構和狀態管理方式。

Ref解包細節 - Additional Ref Unwrapping Details

作為響應式對象屬性

當 ref 被作為響應式對象的屬性訪問或修改時,會自動解包,它的行為像普通屬性

const count = ref(0)
const state = reactive({
count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

如果一個新的 ref 被分配給一個已經鏈接到現有 ref 的屬性,它會替換舊的 ref:

const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
// 原來的 ref 現在與 state.count 斷開連接
console.log(count.value) // 1

ref 的解包僅在嵌套在深度響應式對象內時發生。當作為淺層響應式對象的屬性訪問時不適用。

在數組和集合中的注意事項 - Caveat in Arrays and Collections

與響應式對象不同,當 ref 被作為響應式數組或原生集合類型(如 Map)的元素訪問時,不會進行解包:

const books = reactive([ref('Vue 3 Guide')])
// 需要 .value
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// 需要 .value
console.log(map.get('count').value)

模板中的解包注意事項 - Caveat when Unwrapping in Templates

在模板中,ref 的解包僅在它是模板渲染上下文中的頂級屬性時適用。

在下面的例子中,countobject 是頂級屬性,但 object.id 不是:

const count = ref(0)
const object = { id: ref(1) }

因此,這個表達式正常工作:

{{ count + 1 }}

而這個則不行:

{{ object.id + 1 }}

渲染的結果會是 [object Object]1,因為 object.id 在評估表達式時未被解包並且仍然是 ref 對象。要修復這個問題,我們可以將 id 解構到頂級屬性:

const { id } = object

模板中:

{{ id + 1 }}

現在渲染結果將是 2

需要注意的是,ref 在作為文本插值(即 {{ }} 標籤)的最終評估值時會被解包,因此以下內容將渲染 1

{{ object.id }}

這只是一個文本插值的便利特性,等同於 {{ object.id.value }}

這段看不懂耶!一查原來是JS的語法 www ~
補充說明:const { id } = object 是 JavaScript 中的解構賦值語法,它從 object 對象中提取 id 屬性並將其分配給變量 id
寫文件的人為什麼都寫這麼難懂~原來是我們太嫩 www
分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.