前端摸魚匠
沒有好的理念,只有腳踏實地!
文章目錄
- 一、初識watchEffect:響應式編程的利器
- 1.1 什麼是watchEffect
- 1.2 watchEffect的核心特點
- 1.3 與watch的初步對比
- 二、watchEffect的基本用法
- 2.1 基礎語法結構
- 2.2 監聽ref類型數據
- 2.3 監聽reactive類型數據
- 2.4 監聽多個數據源
- 三、watchEffect的配置選項
- 3.1 flush選項:控制執行時機
- 3.1.1 flush: 'pre'(默認值)
- 3.1.2 flush: 'post'
- 3.1.3 flush: 'sync'
- 3.2 調試選項:onTrack和onTrigger
- 3.2.1 onTrack
- 3.2.2 onTrigger
- 四、watchEffect的高級用法
- 4.1 副作用清理:onInvalidate
- 4.2 停止偵聽
- 4.3 watchPostEffect和watchSyncEffect
- 五、watchEffect的實際應用場景
- 5.1 自動保存用户輸入
- 5.2 響應式DOM操作
- 5.3 路由參數監聽
- 5.4 複雜計算邏輯
- 六、watchEffect與watch的深度對比
- 6.1 核心差異分析
- 6.2 使用場景對比
- 6.3 性能考慮
- 七、watchEffect的內部實現原理
- 7.1 響應式追蹤機制
- 7.2 依賴收集過程
- 7.3 清理機制
- 八、最佳實踐與常見陷阱
- 8.1 最佳實踐
- 8.1.1 合理使用flush選項
- 8.1.2 及時清理副作用
- 8.1.3 避免在副作用中修改依賴
- 8.2 常見陷阱
- 8.2.1 異步操作的依賴追蹤
- 8.2.2 深度監聽的性能問題
- 8.2.3 停止偵聽的時機
一、初識watchEffect:響應式編程的利器
1.1 什麼是watchEffect
在Vue3的Composition API中,watchEffect是一個極其強大的響應式API,它為我們提供了一種自動追蹤依賴並執行副作用的方式。根據官方文檔的定義,watchEffect會立即運行一個函數,同時響應式地追蹤其依賴,並在依賴更改時重新執行【turn0search1】。這意味着我們不需要顯式地指定要監聽的數據源,watchEffect會自動檢測函數內部使用的響應式數據,並在這些數據變化時重新運行函數。
import { ref, watchEffect } from 'vue'
const count = ref(0)
// 立即執行,並自動追蹤count.value作為依賴
watchEffect(() => {
console.log(`計數器的值是: ${count.value}`)
})
1.2 watchEffect的核心特點
watchEffect具有幾個顯著特點,使其在許多場景下比傳統的watch更加便捷:
- 自動依賴收集:不需要手動指定監聽源,函數內使用的響應式數據都會被自動追蹤【turn0search1】
- 立即執行:創建時會立即執行一次,用於建立初始依賴關係【turn0search4】
- 簡潔性:代碼更加簡潔,減少了顯式聲明依賴的需要【turn0search2】
- 響應式追蹤:底層使用Vue的響應式系統,高效追蹤依賴變化【turn0search16】
1.3 與watch的初步對比
雖然watchEffect和watch都能監聽數據變化,但它們在設計理念上有明顯區別:
|
特性
|
watchEffect
|
watch
|
|
依賴追蹤
|
自動收集
|
手動指定
|
|
懶執行
|
否(立即執行)
|
是(默認)
|
|
獲取新舊值
|
否
|
是
|
|
使用場景
|
依賴關係複雜
|
需要精確控制
|
二、watchEffect的基本用法
2.1 基礎語法結構
watchEffect的基本語法非常簡潔,接受兩個參數:一個副作用函數和一個可選的配置對象【turn0search3】。
// 基本語法
watchEffect(
() => {
// 副作用函數內容
},
{
// 可選配置項
flush: 'pre', // 'pre' | 'post' | 'sync'
onTrack: (e) => {}, // 調試鈎子
onTrigger: (e) => {} // 調試鈎子
}
)
2.2 監聽ref類型數據
當監聽ref定義的基本類型數據時,watchEffect會自動追蹤其value屬性的變化【turn0search1】。
計數器: {{ count }}
增加
<script setup>
import { ref, watchEffect } from 'vue'
// 定義ref響應式數據
const count = ref(0)
// 監聽ref數據
watchEffect(() => {
console.log(`count的值變化了: ${count.value}`)
// 這裏會自動追蹤count.value作為依賴
})
const increment = () => {
count.value++
}
</script>
2.3 監聽reactive類型數據
對於reactive定義的對象,watchEffect可以深度追蹤其內部屬性的變化【turn0search1】。
姓名: {{ user.name }}
年齡: {{ user.age }}
更新用户信息
<script setup>
import { reactive, watchEffect } from 'vue'
// 定義reactive響應式對象
const user = reactive({
name: '張三',
age: 25,
address: {
city: '北京'
}
})
// 監聽reactive對象
watchEffect(() => {
console.log(`用户信息: ${user.name}, ${user.age}, ${user.address.city}`)
// 自動追蹤user對象及其嵌套屬性的變化
})
const updateUser = () => {
user.name = '李四'
user.age = 30
user.address.city = '上海'
}
</script>
2.4 監聽多個數據源
watchEffect可以同時監聽多個響應式數據,無需特殊處理,只要在函數中使用這些數據即可【turn0search2】。
import { ref, reactive, watchEffect } from 'vue'
const count = ref(0)
const message = ref('Hello')
const user = reactive({ name: 'Vue', version: 3 })
// 同時監聽多個數據源
watchEffect(() => {
console.log(`計數: ${count.value}, 消息: ${message.value}, 用户: ${user.name} v${user.version}`)
// 自動追蹤所有使用的響應式數據
})
三、watchEffect的配置選項
3.1 flush選項:控制執行時機
flush選項用於控制副作用函數的觸發時機,有三個可選值:‘pre’(默認)、‘post’和’sync’【turn0search1】。
3.1.1 flush: ‘pre’(默認值)
默認情況下,watchEffect會在組件更新之前執行副作用函數【turn0search1】。
import { ref, watchEffect } from 'vue'
const count = ref(0)
// 默認flush: 'pre',在組件更新前執行
watchEffect(() => {
console.log(`pre - count的值: ${count.value}`)
// 此時DOM還未更新
})
count.value++
// 輸出順序: pre - count的值: 1 -> 組件更新
3.1.2 flush: ‘post’
將flush設置為’post’可以使副作用函數在組件更新後執行,這對於需要訪問更新後的DOM元素的場景非常有用【turn0search1】。
import { ref, watchEffect, onMounted } from 'vue'
const count = ref(0)
const elementRef = ref(null)
// flush: 'post',在組件更新後執行
watchEffect(
() => {
console.log(`post - count的值: ${count.value}`)
// 此時DOM已更新,可以訪問更新後的DOM
if (elementRef.value) {
console.log('DOM元素高度:', elementRef.value.clientHeight)
}
},
{ flush: 'post' }
)
count.value++
// 輸出順序: 組件更新 -> post - count的值: 1
3.1.3 flush: ‘sync’
將flush設置為’sync’可以使副作用同步觸發,而不是等到下一個微任務隊列【turn0search1】。這意味着副作用會立即在響應式數據變化時執行。
import { ref, watchEffect } from 'vue'
const count = ref(0)
// flush: 'sync',同步執行
watchEffect(
() => {
console.log(`sync - count的值: ${count.value}`)
// 立即執行,不等待微任務隊列
},
{ flush: 'sync' }
)
count.value++
console.log('同步執行完成')
// 輸出順序: sync - count的值: 1 -> 同步執行完成
⚠️ 注意:sync模式可能會導致性能問題和數據不一致,應當謹慎使用【turn0search7】。
3.2 調試選項:onTrack和onTrigger
watchEffect提供了兩個調試選項onTrack和onTrigger,僅在開發模式下工作,用於調試偵聽器的行為【turn0search5】。
3.2.1 onTrack
onTrack會在響應式property或ref作為依賴項被追蹤時被調用【turn0search13】。
import { ref, watchEffect } from 'vue'
const count = ref(0)
const message = ref('Hello')
watchEffect(
() => {
console.log(count.value + message.value)
},
{
onTrack(e) {
// 當依賴被追蹤時調用
console.log('正在追蹤依賴:', e)
// e包含target(目標對象)、type(追蹤類型)和key(屬性名)等信息
debugger // 可以在這裏設置斷點調試
}
}
)
3.2.2 onTrigger
onTrigger會在依賴項變更導致副作用被觸發時被調用【turn0search13】。
import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(
() => {
console.log(count.value)
},
{
onTrigger(e) {
// 當依賴變更觸發副作用時調用
console.log('依賴變更觸發副作用:', e)
// e包含target、type、key、oldValue和newValue等信息
debugger // 可以在這裏設置斷點調試
}
}
)
count.value++ // 會觸發onTrigger
四、watchEffect的高級用法
4.1 副作用清理:onInvalidate
watchEffect的副作用函數可以接收一個onInvalidate函數作為參數,用於註冊清理回調。清理回調會在該副作用下一次執行前被調用,可以用來清理無效的副作用,例如等待中的異步請求【turn0search3】。
import { ref, watchEffect } from 'vue'
const userId = ref(1)
watchEffect((onInvalidate) => {
// 模擬異步請求
const timer = setTimeout(() => {
console.log(`獲取用户${userId.value}的數據`)
}, 1000)
// 註冊清理函數
onInvalidate(() => {
// 在副作用重新執行前調用
clearTimeout(timer) // 清除上一次的定時器
console.log(`清除用户${userId.value}的請求`)
})
})
// 2秒後改變userId值
setTimeout(() => {
userId.value = 2
}, 2000)
4.2 停止偵聽
watchEffect返回一個用於停止該副作用的函數,調用這個函數可以停止偵聽【turn0search3】。
import { ref, watchEffect } from 'vue'
const count = ref(0)
// 啓動偵聽並獲取停止函數
const stop = watchEffect(() => {
console.log(`count的值: ${count.value}`)
})
// 改變count值,會觸發watchEffect
count.value++ // 輸出: count的值: 1
// 停止偵聽
stop()
// 再次改變count值,不會觸發watchEffect
count.value++ // 無輸出
4.3 watchPostEffect和watchSyncEffect
Vue3還提供了兩個帶預設flush選項的便捷方法:watchPostEffect(flush: ‘post’)和watchSyncEffect(flush: ‘sync’)【turn0search9】。
import { ref, watchPostEffect, watchSyncEffect } from 'vue'
const count = ref(0)
// 等同於watchEffect(..., { flush: 'post' })
watchPostEffect(() => {
console.log(`post effect: ${count.value}`)
})
// 等同於watchEffect(..., { flush: 'sync' })
watchSyncEffect(() => {
console.log(`sync effect: ${count.value}`)
})
五、watchEffect的實際應用場景
5.1 自動保存用户輸入
在表單應用中,可以使用watchEffect自動保存用户輸入到本地存儲【turn0search2】。
輸入內容: {{ userInput }}
<script setup>
import { ref, watchEffect } from 'vue'
const userInput = ref('')
// 自動保存到本地存儲
watchEffect(() => {
if (userInput.value.trim()) {
localStorage.setItem('userInput', userInput.value)
console.log('已保存到本地存儲:', userInput.value)
}
})
// 頁面加載時從本地存儲恢復
const savedInput = localStorage.getItem('userInput')
if (savedInput) {
userInput.value = savedInput
}
</script>
5.2 響應式DOM操作
當需要根據響應式數據變化來操作DOM時,watchEffect非常方便【turn0search2】。
窗口寬度: {{ windowWidth }}px
<script setup>
import { ref, onMounted, watchEffect } from 'vue'
const windowWidth = ref(window.innerWidth)
const resizeDiv = ref(null)
// 監聽窗口大小變化
onMounted(() => {
window.addEventListener('resize', () => {
windowWidth.value = window.innerWidth
})
})
// 根據窗口寬度調整DOM元素
watchEffect(() => {
if (resizeDiv.value) {
if (windowWidth.value < 768) {
resizeDiv.value.style.backgroundColor = 'lightcoral'
resizeDiv.value.style.height = '50px'
} else {
resizeDiv.value.style.backgroundColor = 'lightblue'
resizeDiv.value.style.height = '100px'
}
}
})
</script>
5.3 路由參數監聽
在單頁應用中,可以使用watchEffect監聽路由參數變化並重新獲取數據【turn0search2】。
用户詳情
用户ID: {{ userId }}
用户名: {{ userInfo.name }}
郵箱: {{ userInfo.email }}
<script setup>
import { ref, watchEffect } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const userId = ref(route.params.id)
const userInfo = ref({ name: '', email: '' })
// 模擬獲取用户數據的函數
const fetchUserData = (id) => {
console.log(`獲取用户${id}的數據`)
// 這裏應該是實際的API調用
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: `用户${id}`,
email: `user${id}@example.com`
})
}, 500)
})
}
// 監聽路由參數變化
watchEffect(async () => {
const newUserId = route.params.id
if (newUserId !== userId.value) {
userId.value = newUserId
userInfo.value = await fetchUserData(newUserId)
}
})
</script>
5.4 複雜計算邏輯
當計算邏輯依賴於多個響應式數據,且不需要返回值時,watchEffect比computed更合適【turn0search4】。
購物車
{{ item.name }}
單價: ¥{{ item.price }}
小計: ¥{{ item.price * item.quantity }}
總計: ¥{{ totalPrice }}
運費: ¥{{ shipping }}
應付總額: ¥{{ finalTotal }}
<script setup>
import { reactive, watchEffect, ref } from 'vue'
const cartItems = reactive([
{ id: 1, name: '商品A', price: 100, quantity: 1 },
{ id: 2, name: '商品B', price: 200, quantity: 2 }
])
const totalPrice = ref(0)
const shipping = ref(0)
const finalTotal = ref(0)
// 計算總價和運費
watchEffect(() => {
// 計算商品總價
const subtotal = cartItems.reduce((total, item) => {
return total + item.price * item.quantity
}, 0)
totalPrice.value = subtotal
// 根據總價計算運費
if (subtotal >= 500) {
shipping.value = 0
} else if (subtotal >= 200) {
shipping.value = 10
} else {
shipping.value = 20
}
// 計算最終總額
finalTotal.value = subtotal + shipping.value
// 可以在這裏執行其他副作用,如記錄日誌
console.log(`購物車更新: 總價¥${totalPrice.value}, 運費¥${shipping.value}, 應付¥${finalTotal.value}`)
})
</script>
六、watchEffect與watch的深度對比
6.1 核心差異分析
雖然watchEffect和watch都是用於偵聽響應式數據變化的API,但它們在設計理念和使用方式上有本質區別【turn0search5】。
偵聽需求
需要精確控制依賴?
使用watch
需要立即執行?
使用watchEffect
使用watch
明確指定數據源
自動收集依賴
獲取新舊值
僅獲取當前值
惰性執行
立即執行
6.2 使用場景對比
|
場景
|
推薦使用
|
原因
|
|
需要獲取新舊值
|
watch
|
watch提供新舊值參數
|
|
依賴關係複雜
|
watchEffect
|
自動收集依賴,代碼更簡潔
|
|
需要惰性執行
|
watch
|
watch默認懶執行
|
|
需要立即執行
|
watchEffect
|
watchEffect立即執行一次
|
|
需要精確控制依賴
|
watch
|
手動指定依賴,更可控
|
|
調試依賴關係
|
watchEffect
|
提供onTrack和onTrigger鈎子
|
6.3 性能考慮
在性能方面,watch和watchEffect各有優勢:
- watchEffect:由於自動收集依賴,可能會追蹤不必要的響應式數據,導致過度執行【turn0search6】
- watch:手動指定依賴,可以精確控制回調執行時機,性能更可控【turn0search11】
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
const message = ref('Hello')
// watchEffect會追蹤所有使用的響應式數據
watchEffect(() => {
console.log(count.value) // 即使只關心count,message變化也會觸發重新執行
})
// watch只追蹤指定的數據源
watch(count, () => {
console.log(count.value) // 只有count變化才會觸發
})
七、watchEffect的內部實現原理
7.1 響應式追蹤機制
watchEffect的底層實現基於Vue3的響應式系統,核心是使用ReactiveEffect類來管理副作用函數和依賴關係【turn0search16】。
// 簡化的watchEffect實現原理
function watchEffect(effect, options = {}) {
// 創建副作用函數
const runner = effect(effect, {
lazy: false, // 立即執行
scheduler: job => {
// 調度器,在依賴變化時調用
queueJob(job)
},
...options
})
// 立即執行一次,建立依賴關係
runner()
// 返回停止函數
return () => {
stop(runner)
}
}
7.2 依賴收集過程
當watchEffect執行時,會觸發響應式數據的getter,此時會進行依賴收集【turn0search16】。
watchEffect Effect Reactive Data Dep 創建副作用 訪問響應式數據 觸發getter 收集依賴 建立聯繫 依賴收集完成 數據變化 通知更新 重新執行 watchEffect Effect Reactive Data Dep
7.3 清理機制
watchEffect的清理機制通過onInvalidate函數實現,確保在副作用重新執行前清理之前的副作用【turn0search17】。
// 簡化的清理機制實現
function watchEffect(effect) {
let cleanup
const runner = effect(() => {
// 執行清理函數
if (cleanup) {
cleanup()
}
// 調用用户函數,並傳入清理函數註冊器
effect(onInvalidate => {
cleanup = onInvalidate
})
})
return () => {
// 停止偵聽時也執行清理
if (cleanup) {
cleanup()
}
stop(runner)
}
}
八、最佳實踐與常見陷阱
8.1 最佳實踐
8.1.1 合理使用flush選項
根據實際需求選擇合適的flush選項,避免不必要的性能開銷【turn0search1】。
// 默認pre:適用於大多數場景
watchEffect(() => {
// 默認行為,組件更新前執行
})
// post:需要訪問更新後的DOM
watchEffect(() => {
// 操作DOM
}, { flush: 'post' })
// sync:謹慎使用,僅在必要時
watchEffect(() => {
// 同步執行
}, { flush: 'sync' })
8.1.2 及時清理副作用
使用onInvalidate清理副作用,避免內存泄漏和無效操作【turn0search3】。
watchEffect((onInvalidate) => {
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
.then(response => response.json())
.then(data => {
// 處理數據
})
// 註冊清理函數
onInvalidate(() => {
controller.abort() // 取消請求
})
})
8.1.3 避免在副作用中修改依賴
在副作用中修改被偵聽的響應式數據可能導致無限循環【turn0search4】。
const count = ref(0)
// 可能導致無限循環
watchEffect(() => {
if (count.value < 10) {
count.value++ // 修改了被偵聽的數據
}
})
// 正確做法:使用watch或添加條件判斷
watch(count, (newValue) => {
if (newValue < 10) {
count.value++
}
})
8.2 常見陷阱
8.2.1 異步操作的依賴追蹤
watchEffect僅在其同步執行期間才追蹤依賴,使用異步回調時,只有在第一個await之前訪問到的依賴才會被追蹤【turn0search5】。
const count = ref(0)
const message = ref('Hello')
// 錯誤:message不會被追蹤
watchEffect(async () => {
await new Promise(resolve => setTimeout(resolve, 100))
console.log(message.value) // 這個依賴不會被追蹤
})
// 正確:在await前訪問
watchEffect(async () => {
console.log(message.value) // 會被追蹤
await new Promise(resolve => setTimeout(resolve, 100))
console.log(count.value) // 不會被追蹤
})
8.2.2 深度監聽的性能問題
對於大型對象,watchEffect的深度監聽可能導致性能問題【turn0search11】。
const largeData = reactive({
// 大量嵌套數據
})
// 可能導致性能問題
watchEffect(() => {
// 訪問大型對象會觸發深度監聽
console.log(largeData)
})
// 優化:只監聽需要的屬性
watchEffect(() => {
console.log(largeData.importantProperty)
})
8.2.3 停止偵聽的時機
忘記在組件卸載時停止偵聽可能導致內存泄漏【turn0search3】。
<script setup>
import { ref, watchEffect, onUnmounted } from 'vue'
const data = ref(0)
const stop = watchEffect(() => {
console.log(data.value)
})
// 組件卸載時停止偵聽
onUnmounted(() => {
stop()
})
</script>
watchEffect作為Vue3 Composition API中的重要組成部分,為我們提供了一種簡潔而強大的響應式編程方式。掌握它的特性和最佳實踐,將有助於我們構建更加高效、可維護的Vue3應用。