watch 函數分為兩部分:監視的數據源,要執行的動作。
先看一個列子,列出了幾種常用數據源的情況:

  • ref變量,
  • 多個 ref 變量形成一個數據源,
  • reactive 變量,默認使用深度監聽,不需要像早期版本那樣額外標明deep
  • 函數。

點擊查看代碼

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

const count = ref(0)
const message = ref('Hello')

// 基礎用法:監聽單個數據源
watch(count, (newValue, oldValue) => {
  console.log(`count changed from ${oldValue} to ${newValue}`)
})

// 監聽多個數據源
watch([count, message], ([newCount, newMsg], [oldCount, oldMsg]) => {
  console.log(`count: ${oldCount} -> ${newCount}`)
  console.log(`message: ${oldMsg} -> ${newMsg}`)
})

// 監聽響應式對象
const user = reactive({
  name: 'John',
  age: 20
})

//不需要 deep 選項:深度監聽對象的所有屬性變化
watch(user, (newValue, oldValue) => {
  console.log('user changed:', newValue)
}/*, { deep: true }*/)

</script>

我們看到vue源代碼對數據源是怎麼處理的:

  • 首先數據源必須是 getter 函數,如果不是將其轉化為 getter 函數
    ** 將 ref 變量轉化成 getter 函數。
    ** 將 reactive 變量轉化成 getter 函數。
    ** 將 數組 轉化成 getter 函數
    ** 將 函數 轉化成 getter 函數

點擊查看代碼

let getter: () => any
let forceTrigger = false
let isMultiSource = false

// 1. 處理 ref 類型
if (isRef(source)) {
  getter = () => source.value
  forceTrigger = isShallow(source)
}
// 2. 處理 reactive 對象
else if (isReactive(source)) {
  getter = () => reactiveGetter(source)
  forceTrigger = true
}
// 3. 處理數組(多數據源)
else if (isArray(source)) {
  isMultiSource = true
  forceTrigger = source.some(s => isReactive(s) || isShallow(s))
  getter = () =>
    source.map(s => {
      if (isRef(s)) {
        return s.value
      } else if (isReactive(s)) {
        return reactiveGetter(s)
      } else if (isFunction(s)) {
        return call ? call(s, WatchErrorCodes.WATCH_GETTER) : s()
      } else {
        __DEV__ && warnInvalidSource(s)
      }
    })
}
// 4. 處理函數
else if (isFunction(source)) {
  if (cb) {
    // getter with cb
    getter = call
      ? () => call(source, WatchErrorCodes.WATCH_GETTER)
      : (source as () => any)
  } else {
    // watchEffect
    getter = () => {
      if (cleanup) {
        pauseTracking()
        try {
          cleanup()
        } finally {
          resetTracking()
        }
      }
      const currentEffect = activeWatcher
      activeWatcher = effect
      try {
        return call
          ? call(source, WatchErrorCodes.WATCH_CALLBACK, [boundCleanup])
          : source(boundCleanup)
      } finally {
        activeWatcher = currentEffect
      }
    }
  }
}
// 5. 處理無效數據源
else {
  getter = NOOP
  __DEV__ && warnInvalidSource(source)
}

所以:

  • ref 轉化成 ()=>xxx.value.
    ** 注意這裏很明顯只監視xxx.value的變化,所以只追蹤了一層,所以:
    *** 如果 ref 變量本身包含的是簡單值,比如number,string,bool,那麼沒問題。
    *** 如果 ref 變量本身包含的是對象值,比如object,array 等等,那麼不會深層監視,只有對象整個變化了才能觸發watch動作。
  • reactive 轉化成 () => reactiveGetter(xxx)
    ** 看下面reactiveGetter()函數,可以看到默認路徑是深度跟蹤的。
  • 函數形式,直接用函數作為 getter 函數

點擊查看代碼

const reactiveGetter = (source: object) => {
    //如果用option來指明深度跟蹤,我們統一到後面處理,這裏就返回簡單的一層。
    // traverse will happen in wrapped getter below
    if (deep) return source
    //如果用option來指明不深度 deep: false | 0, 那麼只追蹤一層。
    // for `deep: false | 0` or shallow reactive, only traverse root-level properties
    if (isShallow(source) || deep === false || deep === 0)
      return traverse(source, 1)
    //默認路徑是深度跟蹤的。
    // for `deep: undefined` on a reactive object, deeply traverse all properties
    return traverse(source)
  }

所以,

  1. 簡單基本數據,建議使用 ref 作為數據源,只監視一層。
  2. 簡單對象數據,建議使用 reactive 作為數據源,深度監視。比如只有1層的對象。
  3. 複雜對象數據,建議使用函數明確的挑出追蹤數據點。
  4. 頭腦比較混亂時,一律使用函數明確要監視的數據點
    最後,
    因為 vue 推薦使用 ref 包裝對象,所以我們很多時候會用 ref 包裝對象。那麼要監視這種ref對象的內部變動就有點特殊,因為
    最為 ref ,傳遞給 watch 是隻能監視一層。沒法深層監視。那麼:
  • 要麼使用 函數 來標明要監視的數據點,
  • 要麼額外加 deep 屬性,實現全部深度監視。