Appearance
侦听器
侦听器允许我们在响应式数据变化时执行副作用。Vue 提供了两种主要的侦听器:watch 和 watchEffect。
基本示例
watch
watch 函数用于侦听一个或多个响应式数据源,并在数据变化时执行回调函数。
vue
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.indexOf('?') > -1) {
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
}
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</template>watchEffect
watchEffect 会立即执行一遍回调函数,如果这时函数产生了副作用,Vue 会自动追踪副作用的依赖关系,并在其依赖变更时重新运行该函数。
vue
<script setup>
import { ref, watchEffect } from 'vue'
const todoId = ref(1)
const data = ref(null)
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
</script>
<template>
<p>Todo id: {{ todoId }}</p>
<button @click="todoId++">Fetch next todo</button>
<p v-if="!data">Loading...</p>
<pre v-else>{{ data }}</pre>
</template>侦听数据源类型
侦听单个源
javascript
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}`)
})侦听响应式对象
javascript
const obj = reactive({ count: 0 })
// 这不能正常工作,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)深层侦听器
直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:
javascript
const obj = reactive({ count: 0, nested: { id: 1 } })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
obj.count++
obj.nested.id++相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
javascript
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)你也可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器:
javascript
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)即时回调的侦听器
watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。
我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行:
javascript
watch(source, (newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })回调的触发时机
当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项:
javascript
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect():
javascript
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})停止侦听器
在 setup() 或 <script setup> 中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,你无需关心怎么停止一个侦听器。
一个关键点是,侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。
vue
<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:
javascript
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()注意,需要异步创建侦听器的情况很少,请尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑:
javascript
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 数据加载后执行某些操作...
}
})选项式 API
watch 选项
javascript
export default {
data() {
return {
question: '',
answer: 'Questions usually contain a question mark. ;-)'
}
},
watch: {
// 每当 question 改变时,这个函数就会执行
question(newQuestion, oldQuestion) {
if (newQuestion.indexOf('?') > -1) {
this.getAnswer()
}
}
},
methods: {
async getAnswer() {
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
}
}
}
}深层侦听器
javascript
export default {
watch: {
someObject: {
handler(newValue, oldValue) {
// 注意:在嵌套的变更中,
// 只要没有替换对象本身,
// 那么这里的 `newValue` 和 `oldValue` 相同
},
deep: true
}
}
}即时回调的侦听器
javascript
export default {
watch: {
question: {
handler(newQuestion, oldQuestion) {
// ...
},
// 强制立即执行回调
immediate: true
}
}
}回调数组
javascript
export default {
watch: {
// 字符串方法名
a: 'someMethod',
// 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
c: {
handler: 'someMethod',
deep: true
},
// 侦听单个嵌套属性:
'c.d': function (val, oldVal) {
// do something
},
// 该回调将会在侦听开始之后被立即调用
e: {
handler(val, oldVal) {
// do something
},
immediate: true
},
// 你可以传入回调数组,它们会被逐一调用
f: [
'handle1',
function handle2(val, oldVal) {
// do something
},
{
handler: function handle3(val, oldVal) {
// do something
}
}
]
},
methods: {
someMethod() {
// do something
},
handle1() {
// do something
}
}
}this.$watch()
我们也可以使用组件实例的 $watch() 方法来命令式地创建一个侦听器:
javascript
export default {
created() {
this.$watch('question', (newQuestion) => {
// ...
})
}
}如果要在特定条件下设置一个侦听器,或者只侦听响应用户交互的内容,这方法很有用。它还允许你提前停止该侦听器。
实际应用示例
表单验证
vue
<script setup>
import { ref, watch, computed } from 'vue'
const email = ref('')
const password = ref('')
const confirmPassword = ref('')
const emailError = ref('')
const passwordError = ref('')
const confirmPasswordError = ref('')
// 邮箱验证
watch(email, (newEmail) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (newEmail && !emailRegex.test(newEmail)) {
emailError.value = '请输入有效的邮箱地址'
} else {
emailError.value = ''
}
})
// 密码验证
watch(password, (newPassword) => {
if (newPassword && newPassword.length < 6) {
passwordError.value = '密码长度至少为6位'
} else {
passwordError.value = ''
}
})
// 确认密码验证
watch([password, confirmPassword], ([newPassword, newConfirmPassword]) => {
if (newConfirmPassword && newPassword !== newConfirmPassword) {
confirmPasswordError.value = '两次输入的密码不一致'
} else {
confirmPasswordError.value = ''
}
})
const isFormValid = computed(() => {
return email.value &&
password.value &&
confirmPassword.value &&
!emailError.value &&
!passwordError.value &&
!confirmPasswordError.value
})
</script>
<template>
<form>
<div>
<input v-model="email" placeholder="邮箱" />
<span v-if="emailError" class="error">{{ emailError }}</span>
</div>
<div>
<input v-model="password" type="password" placeholder="密码" />
<span v-if="passwordError" class="error">{{ passwordError }}</span>
</div>
<div>
<input v-model="confirmPassword" type="password" placeholder="确认密码" />
<span v-if="confirmPasswordError" class="error">{{ confirmPasswordError }}</span>
</div>
<button :disabled="!isFormValid" type="submit">注册</button>
</form>
</template>数据持久化
vue
<script setup>
import { ref, watch } from 'vue'
const preferences = ref({
theme: 'light',
language: 'zh-CN',
notifications: true
})
// 从 localStorage 加载设置
const loadPreferences = () => {
const saved = localStorage.getItem('userPreferences')
if (saved) {
preferences.value = { ...preferences.value, ...JSON.parse(saved) }
}
}
// 保存设置到 localStorage
watch(
preferences,
(newPreferences) => {
localStorage.setItem('userPreferences', JSON.stringify(newPreferences))
},
{ deep: true }
)
// 组件挂载时加载设置
loadPreferences()
</script>
<template>
<div>
<h3>用户设置</h3>
<label>
主题:
<select v-model="preferences.theme">
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
</label>
<label>
语言:
<select v-model="preferences.language">
<option value="zh-CN">中文</option>
<option value="en-US">English</option>
</select>
</label>
<label>
<input
type="checkbox"
v-model="preferences.notifications"
/>
启用通知
</label>
</div>
</template>搜索防抖
vue
<script setup>
import { ref, watch } from 'vue'
const searchQuery = ref('')
const searchResults = ref([])
const isLoading = ref(false)
let searchTimeout = null
const performSearch = async (query) => {
if (!query.trim()) {
searchResults.value = []
return
}
isLoading.value = true
try {
// 模拟 API 调用
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`)
const results = await response.json()
searchResults.value = results
} catch (error) {
console.error('搜索失败:', error)
searchResults.value = []
} finally {
isLoading.value = false
}
}
// 防抖搜索
watch(searchQuery, (newQuery) => {
// 清除之前的定时器
if (searchTimeout) {
clearTimeout(searchTimeout)
}
// 设置新的定时器
searchTimeout = setTimeout(() => {
performSearch(newQuery)
}, 300)
})
</script>
<template>
<div>
<input
v-model="searchQuery"
placeholder="搜索..."
class="search-input"
/>
<div v-if="isLoading" class="loading">
搜索中...
</div>
<ul v-else-if="searchResults.length" class="results">
<li v-for="result in searchResults" :key="result.id">
{{ result.title }}
</li>
</ul>
<div v-else-if="searchQuery.trim()" class="no-results">
没有找到相关结果
</div>
</div>
</template>性能考虑
避免在侦听器中创建大量对象
javascript
// 不好
watch(someRef, () => {
// 每次都创建新对象
const expensiveObject = createExpensiveObject()
doSomething(expensiveObject)
})
// 好
const expensiveObject = createExpensiveObject()
watch(someRef, () => {
// 重用对象
doSomething(expensiveObject)
})使用 shallowRef 避免深层响应式
javascript
import { shallowRef, watch } from 'vue'
// 对于大型对象,使用 shallowRef 可以提高性能
const largeObject = shallowRef({
// 大量数据...
})
watch(largeObject, (newValue) => {
// 只有当整个对象被替换时才会触发
}, { deep: false })条件性侦听
javascript
const shouldWatch = ref(true)
const data = ref('')
// 只在需要时进行侦听
watchEffect(() => {
if (shouldWatch.value) {
// 执行侦听逻辑
console.log('Data changed:', data.value)
}
})调试侦听器
使用 onTrack 和 onTrigger
javascript
watchEffect(
() => {
// 副作用函数
console.log(count.value)
},
{
onTrack(e) {
// 当 count.value 被追踪为依赖时触发
debugger
},
onTrigger(e) {
// 当 count.value 被修改时触发
debugger
}
}
)