Vue3 筆記
Vue3 筆記
關于ref 和 reactive
ref是一個創建響應式的API,它可以將普通的 JavaScript數據(基本類型和復雜類型) 變為響應式數據。ref函數接受一個初始值作為參數,并返回一個響應式的引用對象。reactive是一個用于創建響應式對象的API。它接受一個普通的 JavaScript對象(復雜類型) 作為參數,并返回一個響應式的代理。
reactive底層是通過Proxy來實現數據劫持的,創建代理對象,可操作的數據必須是對象,不能是原始類型。
ref通過對原始類型包裹在一個帶有.value屬性的特殊對象中,再使用Proxy劫持該對象,并返回代理對象。非原始類型傳遞給ref,將通過reactive轉換為響應式代理對象。
reactive的局限性
- 有限的值類型:它只能用于對象類型 (對象、數組和如
Map、Set這樣的集合類型)。它不能持有如string、number或boolean這樣的原始類型。- 不能替換整個對象:由于 Vue 的響應式跟蹤是通過屬性訪問實現的,因此我們必須始終保持對響應式對象的相同引用。這意味著我們不能輕易地“替換”響應式對象,因為這樣的話與第一個引用的響應性連接將丟失:
let state = reactive({ count: 0 }) // 上面的 ({ count: 0 }) 引用將不再被追蹤 // (響應性連接已丟失!) state = reactive({ count: 1 })
監聽器
監聽器,能在狀態變化時執行一些“副作用”:例如更改 DOM,或是根據異步操作的結果去修改另一處的狀態。
watch
在組合式 API 中,我們可以使用
watch函數在每次響應式狀態發生變化時觸發回調函數:
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)
// 可以直接偵聽一個 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
} finally {
loading.value = false
}
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>
偵聽數據源類型
watch的第一個參數可以是不同形式的“數據源”:它可以是一個 ref (包括計算屬性)、一個響應式對象、一個getter函數 、或多個數據源組成的數組:
const x = ref(0)
const y = ref(0)
// 單個 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter 函數
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// 多個來源組成的數組
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
注意,你不能直接偵聽響應式對象的屬性值,例如:
const obj = reactive({ count: 0 })
// 錯誤,因為 watch() 得到的參數是一個 number
watch(obj.count, (count) => {
console.log(`Count is: ${count}`)
})
這里需要用一個返回該屬性的 getter 函數:
// 提供一個 getter 函數
watch(
() => obj.count,
(count) => {
console.log(`Count is: ${count}`)
}
)
深層偵聽器
直接給
watch()傳入一個響應式對象,會隱式地創建一個深層偵聽器——該回調函數在所有嵌套的變更時都會被觸發。
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的屬性變更時觸發
// 注意:`newValue` 此處和 `oldValue` 是相等的
// 因為它們是同一個對象!
})
obj.count++
相比之下,一個返回響應式對象的 getter 函數,只有在返回不同的對象時,才會觸發回調:
watch(
() => state.someObject,
() => {
// 僅當 state.someObject 被替換時觸發
}
)
你也可以給上面這個例子顯式地加上
deep選項,強制轉成深層偵聽器:
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此處和 `oldValue` 是相等的
// *除非* state.someObject 被整個替換了
},
{ deep: true }
)
即時回調的偵聽器
watch默認是懶執行的:僅當數據源變化時,才會執行回調。如果我們希望在創建偵聽器時,立即執行一遍回調。舉例來說,我們想請求一些初始數據,然后在相關狀態更改時重新請求數據。我們可以通過傳入
immediate: true選項來強制偵聽器的回調立即執行:
watch(
source,
(newValue, oldValue) => {
// 立即執行,且當 `source` 改變時再次執行
},
{ immediate: true }
)
一次性偵聽器
每當被偵聽源發生變化時,偵聽器的回調就會執行。如果希望回調只在源變化時觸發一次,請使用
once: true選項。
watchEffect
watchEffect()允許我們自動跟蹤回調的響應式依賴。
const todoId = ref(1)
const data = ref(null)
watch(
todoId,
async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
},
{ immediate: true }
)
// 利用watchEffect偵聽器可以重寫為:
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
這個例子中,回調會立即執行,不需要指定
immediate: true。在執行期間,它會自動追蹤todoId.value作為依賴(和計算屬性類似)。每當todoId.value變化時,回調會再次執行。有了watchEffect(),我們不再需要明確傳遞todoId作為源值。
對于這種只有一個依賴項的例子來說,
watchEffect()的好處相對較小。但是對于有多個依賴項的偵聽器來說,使用
watchEffect()可以消除手動維護依賴列表的負擔。此外,如果你需要偵聽一個嵌套數據結構中的幾個屬性,
watchEffect()可能會比深度偵聽器更有效,因為它將只跟蹤回調中被使用到的屬性,而不是遞歸地跟蹤所有的屬性。
注意:watchEffect僅會在其同步執行期間,才追蹤依賴。在使用異步回調時,只有在第一個await正常工作前訪問到的屬性才會被追蹤。
watch vs. watchEffect
watch和watchEffect都能響應式地執行有副作用的回調。它們之間的主要區別是追蹤響應式依賴的方式:
watch只追蹤明確偵聽的數據源。它不會追蹤任何在回調中訪問到的東西。另外,僅在數據源確實改變時才會觸發回調。watch會避免在發生副作用時追蹤依賴,因此,我們能更加精確地控制回調函數的觸發時機。watchEffect,則會在副作用發生期間追蹤依賴。它會在同步執行過程中,自動追蹤所有能訪問到的響應式屬性。這更方便,而且代碼往往更簡潔,但有時其響應性依賴關系會不那么明確。
副作用清理
一個回調函數,當偵聽器失效并準備重新運行時會被調用。
onWatcherCleanup()
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// 回調邏輯
})
onWatcherCleanup(() => {
// 終止過期請求
controller.abort()
})
})
onWatcherCleanup僅在 Vue 3.5+ 中支持,并且必須在watchEffect效果函數或watch回調函數的同步執行期間調用:你不能在異步函數的await語句之后調用它。
onCleanup()
作為替代,
onCleanup函數還作為第三個參數傳遞給偵聽器回調,以及watchEffect作用函數的第一個參數:
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// 清理邏輯
})
})
watchEffect((onCleanup) => {
// ...
onCleanup(() => {
// 清理邏輯
})
})
這在 3.5 之前的版本有效。此外,通過函數參數傳遞的
onCleanup與偵聽器實例相綁定,因此不受onWatcherCleanup的同步限制。
回調的觸發時機
當你更改了響應式狀態,它可能會同時觸發 Vue 組件更新和偵聽器回調。
類似于組件更新,用戶創建的偵聽器回調函數也會被批量處理以避免重復調用。例如,如果我們同步將一千個項目推入被偵聽的數組中,我們可能不希望偵聽器觸發一千次。
默認情況下,偵聽器回調會在父組件更新 (如有) 之后、所屬組件的 DOM 更新之前被調用。這意味著如果你嘗試在偵聽器回調中訪問所屬組件的 DOM,那么 DOM 將處于更新前的狀態。
Post Watchers
如果想在偵聽器回調中能訪問被 Vue 更新之后的所屬組件的 DOM,你需要指明
flush: 'post'選項:
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
后置刷新的
watchEffect()有個更方便的別名watchPostEffect():
同步偵聽器
你還可以創建一個同步觸發的偵聽器,它會在 Vue 進行任何更新之前觸發:
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})
同步觸發的
watchEffect()有個更方便的別名watchSyncEffect():
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* 在響應式數據變化時同步執行 */
})
停止偵聽器
在
setup()或<script setup>中用同步語句創建的偵聽器,會自動綁定到宿主組件實例上,并且會在宿主組件卸載時自動停止。因此,在大多數情況下,你無需關心怎么停止一個偵聽器。
一個關鍵點是,偵聽器必須用同步語句創建:如果用異步回調創建一個偵聽器,那么它不會綁定到當前組件上,你必須手動停止它,以防內存泄漏。如下方這個例子:
<script setup>
import { watchEffect } from 'vue'
// 它會自動停止
watchEffect(() => {})
// ...這個則不會!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
要手動停止一個偵聽器,請調用
watch或watchEffect返回的函數:
const unwatch = watchEffect(() => {})
// ...當該偵聽器不再需要時
unwatch()

浙公網安備 33010602011771號