跳转到内容

监听器

基本示例

计算属性允许我们声明式地计算派生值。然而,在某些情况下,我们需要根据状态变化执行“副作用” - 例如,修改 DOM,或根据异步操作的结果更改另一部分状态。

使用 Options API,我们可以使用 watch 选项 来触发函数,当响应式属性变化时

js
export default {
  data() {
    return {
      question: '',
      answer: 'Questions usually contain a question mark. ;-)',
      loading: false
    }
  },
  watch: {
    // whenever question changes, this function will run
    question(newQuestion, oldQuestion) {
      if (newQuestion.includes('?')) {
        this.getAnswer()
      }
    }
  },
  methods: {
    async getAnswer() {
      this.loading = true
      this.answer = 'Thinking...'
      try {
        const res = await fetch('https://yesno.wtf/api')
        this.answer = (await res.json()).answer
      } catch (error) {
        this.answer = 'Error! Could not reach the API. ' + error
      } finally {
        this.loading = false
      }
    }
  }
}
template
<p>
  Ask a yes/no question:
  <input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>

在游乐场中尝试

watch 选项还支持以点分隔的路径作为键

js
export default {
  watch: {
    // Note: only simple paths. Expressions are not supported.
    'some.nested.key'(newValue) {
      // ...
    }
  }
}

使用 Composition API,我们可以使用 watch 函数 来触发回调,当任何响应式状态变化时

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

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)

// watch works directly on a 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(包括计算 ref)、一个响应式对象、一个 getter 函数,或者多个源的数组

js
const x = ref(0)
const y = ref(0)

// single ref
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

// getter
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)

// array of multiple sources
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

请注意,您不能像这样监听响应式对象的属性

js
const obj = reactive({ count: 0 })

// this won't work because we are passing a number to watch()
watch(obj.count, (count) => {
  console.log(`Count is: ${count}`)
})

相反,请使用 getter

js
// instead, use a getter:
watch(
  () => obj.count,
  (count) => {
    console.log(`Count is: ${count}`)
  }
)

深度监听器

watch 默认为浅度监听:只有当被监听的属性被赋予新值时,回调才会触发 - 它不会在嵌套属性变化时触发。如果你想回调在所有嵌套变化上触发,你需要使用深度监听。

js
export default {
  watch: {
    someObject: {
      handler(newValue, oldValue) {
        // Note: `newValue` will be equal to `oldValue` here
        // on nested mutations as long as the object itself
        // hasn't been replaced.
      },
      deep: true
    }
  }
}

当你直接在响应式对象上调用 watch() 时,它将隐式创建一个深度监听 - 回调将在所有嵌套变化上触发。

js
const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // fires on nested property mutations
  // Note: `newValue` will be equal to `oldValue` here
  // because they both point to the same object!
})

obj.count++

这应该与返回响应式对象的 getter 进行区分 - 在后一种情况下,只有当 getter 返回不同的对象时,回调才会触发。

js
watch(
  () => state.someObject,
  () => {
    // fires only when state.someObject is replaced
  }
)

然而,你可以通过显式使用 deep 选项将第二种情况强制转换为深度监听。

js
watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // Note: `newValue` will be equal to `oldValue` here
    // *unless* state.someObject has been replaced
  },
  { deep: true }
)

在 Vue 3.5+ 中,deep 选项也可以是一个数字,表示最大遍历深度 - 即 Vue 应该遍历对象嵌套属性多少层。

谨慎使用

深度监听需要遍历被监听对象的所有嵌套属性,在大型数据结构中使用时可能会很昂贵。仅在必要时使用,并注意性能影响。

急切监听器

watch 默认为惰性:回调不会在被监听源改变时调用。但在某些情况下,我们可能希望相同的回调逻辑被急切执行 - 例如,我们可能想要获取一些初始数据,然后每当相关状态改变时重新获取数据。

我们可以通过声明一个带有 handler 函数和 immediate: true 选项的对象来强制执行一个监听器的回调,立即执行。

js
export default {
  // ...
  watch: {
    question: {
      handler(newQuestion) {
        // this will be run immediately on component creation.
      },
      // force eager callback execution
      immediate: true
    }
  }
  // ...
}

handler 函数的初始执行将在 created 钩子之前发生。Vue 已经处理了 datacomputedmethods 选项,所以这些属性将在第一次调用中可用。

我们还可以通过传递 immediate: true 选项来强制执行一个监听器的回调,立即执行。

js
watch(
  source,
  (newValue, oldValue) => {
    // executed immediately, then again when `source` changes
  },
  { immediate: true }
)

一次性监听器

  • 仅支持 3.4+

监听器的回调会在被监听源改变时执行。如果你想回调在被源改变时只触发一次,请使用 once: true 选项。

js
export default {
  watch: {
    source: {
      handler(newValue, oldValue) {
        // when `source` changes, triggers only once
      },
      once: true
    }
  }
}
js
watch(
  source,
  (newValue, oldValue) => {
    // when `source` changes, triggers only once
  },
  { once: true }
)

watchEffect()

监听器的回调通常使用与源完全相同的响应式状态。例如,考虑以下代码,它使用一个监听器在 todoId ref 变化时加载远程资源。

js
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 }
)

特别是,请注意监听器如何两次使用 todoId,一次作为源,然后再次在回调中。

这可以通过 watchEffect() 简化。 watchEffect() 允许我们自动跟踪回调的响应式依赖。上面的监听器可以重写为

js
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() 可能比深层次监视器更高效,因为它只会跟踪回调中使用的属性,而不是递归地跟踪所有属性。

提示

watchEffect 只在其 同步 执行期间跟踪依赖项。当与异步回调一起使用时,只有在前一个 await 调用之前访问的属性才会被跟踪。

watchwatchEffect

watchwatchEffect 都允许我们进行反应式副作用。它们的主要区别在于它们跟踪反应式依赖项的方式。

  • watch 只跟踪显式监视的源。它不会跟踪回调内部访问的内容。此外,回调仅在源实际更改时触发。watch 将依赖项跟踪与副作用分开,使我们能够更精确地控制回调何时触发。

  • 另一方面,watchEffect 将依赖项跟踪和副作用合并为一个阶段。它将自动跟踪其在同步执行期间访问的每个反应式属性。这更方便,通常会产生更简洁的代码,但使得其反应式依赖项不够明确。

副作用清理

有时我们可能在监视器中执行副作用,例如异步请求。

js
watch(id, (newId) => {
  fetch(`/api/${newId}`).then(() => {
    // callback logic
  })
})
js
export default {
  watch: {
    id(newId) {
      fetch(`/api/${newId}`).then(() => {
        // callback logic
      })
    }
  }
}

但如果在请求完成之前 id 发生了变化怎么办?当先前的请求完成时,它仍然会使用已经过时的 ID 值触发回调。理想情况下,我们希望在 id 变为新值时取消过时的请求。

我们可以使用 onWatcherCleanup() API 来注册一个清理函数,该函数将在监视器被无效化和即将重新运行时被调用

js
import { watch, onWatcherCleanup } from 'vue'

watch(id, (newId) => {
  const controller = new AbortController()

  fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
    // callback logic
  })

  onWatcherCleanup(() => {
    // abort stale request
    controller.abort()
  })
})
js
import { onWatcherCleanup } from 'vue'

export default {
  watch: {
    id(newId) {
      const controller = new AbortController()

      fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
        // callback logic
      })

      onWatcherCleanup(() => {
        // abort stale request
        controller.abort()
      })
    }
  }
}

请注意,onWatcherCleanup 仅在 Vue 3.5+ 中受支持,并且必须在 watchEffect 效果函数或 watch 回调函数的同步执行期间调用:您不能在异步函数中的 await 语句之后调用它。

或者,一个 onCleanup 函数也被作为第三个参数传递给监视器回调,并且作为第一个参数传递给 watchEffect 效果函数

js
watch(id, (newId, oldId, onCleanup) => {
  // ...
  onCleanup(() => {
    // cleanup logic
  })
})

watchEffect((onCleanup) => {
  // ...
  onCleanup(() => {
    // cleanup logic
  })
})
js
export default {
  watch: {
    id(newId, oldId, onCleanup) {
      // ...
      onCleanup(() => {
        // cleanup logic
      })
    }
  }
}

这在版本 3.5 之前有效。此外,通过函数参数传递的 onCleanup 绑定到监视器实例,因此它不受 onWatcherCleanup 的同步约束。

回调刷新时机

当您修改反应式状态时,它可能会触发 Vue 组件更新和您创建的监视器回调。

类似于组件更新,用户创建的监视器回调被批量处理以避免重复调用。例如,如果我们同步地将一千个项目推送到被监视的数组中,我们可能不希望监视器触发一千次。

默认情况下,监视器的回调函数将在父组件更新(如果有)之后、以及所属组件的DOM更新之前被调用。这意味着如果您在监视器回调函数中尝试访问所属组件的自身DOM,DOM将处于更新前的状态。

后置监视

如果您想在Vue更新所属组件的DOM之后在监视器回调中访问所属组件的DOM,您需要指定flush: 'post'选项

js
export default {
  // ...
  watch: {
    key: {
      handler() {},
      flush: 'post'
    }
  }
}
js
watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})

后置刷新watchEffect()还有一个便利别名,watchPostEffect()

js
import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* executed after Vue updates */
})

同步监视

也可以创建一个在Vue管理的任何更新之前触发的同步监视器

js
export default {
  // ...
  watch: {
    key: {
      handler() {},
      flush: 'sync'
    }
  }
}
js
watch(source, callback, {
  flush: 'sync'
})

watchEffect(callback, {
  flush: 'sync'
})

同步watchEffect()还有一个便利别名,watchSyncEffect()

js
import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
  /* executed synchronously upon reactive data change */
})

谨慎使用

同步监视器没有批处理,并且在检测到每次响应式变异时都会触发。它们可以用来监视简单的布尔值,但避免在可能同步多次变异的数据源上使用,例如数组。

this.$watch()

还可以使用$watch()实例方法显式地创建监视器

js
export default {
  created() {
    this.$watch('question', (newQuestion) => {
      // ...
    })
  }
}

这在需要条件性地设置监视器,或者仅对用户交互做出响应时非常有用。它还允许您提前停止监视器。

停止监视器

使用watch选项或$watch()实例方法声明的监视器将在所属组件卸载时自动停止,所以在大多数情况下您不需要担心手动停止监视器。

在极少数需要在该组件卸载之前停止监视器的情况下,$watch() API将返回一个用于此目的的函数

js
const unwatch = this.$watch('foo', callback)

// ...when the watcher is no longer needed:
unwatch()

setup()<script setup>内部同步声明的监视器绑定到所属组件实例,并在所属组件卸载时自动停止。在大多数情况下,您不需要担心手动停止监视器。

关键在于监视器必须同步创建:如果监视器在异步回调中创建,它将不会绑定到所属组件,并且必须手动停止以避免内存泄漏。以下是一个示例

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

// this one will be automatically stopped
watchEffect(() => {})

// ...this one will not!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

要手动停止监视器,请使用返回的句柄函数。这对于watchwatchEffect都适用

js
const unwatch = watchEffect(() => {})

// ...later, when no longer needed
unwatch()

请注意,在极少数情况下需要创建异步监视器,并且尽可能优先使用同步创建。如果您需要等待一些异步数据,您可以将您的监视逻辑条件化

js
// data to be loaded asynchronously
const data = ref(null)

watchEffect(() => {
  if (data.value) {
    // do something when data is loaded
  }
})
观察者已加载