Vue 3 的watchEffect函數:介紹watchEffect的基礎用法和特點_數據源


前端摸魚匠


沒有好的理念,只有腳踏實地!


文章目錄

  • 一、初識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應用。