Vue.js 基础知识(三)
十一、侦听器
watch()
const question = ref('')
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) { /* 请求数据 */ }
})
侦听数据源类型:
const x = ref(0)
const y = ref(0)
const obj = reactive({ count: 0 })
watch(x, (newX) => { }) // 单个 ref
watch(() => x.value + y.value, (sum) => { }) // getter 函数
watch([x, () => y.value], ([newX, newY]) => { }) // 多个源数组
watch(() => obj.count, (count) => { }) // reactive 对象属性(必须用 getter)
watch(obj, callback) // reactive 对象(隐式深层侦听)
不能直接侦听 reactive 对象的属性
watch(obj.count, ...),因为参数值是一个数字。
常用选项
watch(source, callback, { immediate: true }) // 创建时立即执行
watch(source, callback, { once: true }) // 只触发一次 (Vue 3.4+)
watch(() => state.obj, handler, { deep: true }) // 显式深层侦听
- 传入 reactive 对象时隐式深层侦听,但
newValue === oldValue(同一引用) - 深层侦听对大型数据结构开销大,Vue 3.5+ 可指定
deep: 数字限制遍历深度
watchEffect()
自动追踪回调内访问的所有响应式依赖,创建时立即执行:
watchEffect(async () => {
const response = await fetch(`https://api.example.com/item/${id.value}`)
data.value = await response.json()
})
watchEffect仅追踪同步执行期间访问的依赖。第一个await之后的属性不会被追踪。
watch vs watchEffect
| 特性 | watch |
watchEffect |
|---|---|---|
| 依赖追踪 | 显式声明数据源 | 自动追踪回调内所有依赖 |
| 懒执行 | 是(除非 immediate) |
否(创建时立即执行) |
| 访问旧值 | 可以 | 不可以 |
副作用清理
// Vue 3.5+
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal })
onWatcherCleanup(() => controller.abort())
})
// 旧写法(仍可用)
watch(id, (newId, oldId, onCleanup) => {
onCleanup(() => { /* 清理 */ })
})
回调触发时机
watch(source, callback, { flush: 'post' }) // DOM 更新后触发
watchPostEffect(() => { }) // flush: 'post' 的别名
watchSyncEffect(() => { }) // flush: 'sync' 同步触发
默认时机:父组件更新后、组件自身 DOM 更新前。
停止侦听器
const unwatch = watchEffect(() => { /* ... */ })
unwatch() // 手动停止
在
setup()中同步创建的侦听器会在组件卸载时自动停止。异步创建的(如setTimeout内)不会自动停止。
十二、模板引用
访问 DOM 元素
推荐方式 (Vue 3.5+):
<script setup>
import { useTemplateRef, onMounted } from 'vue'
const input = useTemplateRef('my-input')
onMounted(() => { input.value.focus() })
</script>
<template>
<input ref="my-input" />
</template>
旧方式 (3.5 以下):
<script setup>
import { ref, onMounted } from 'vue'
const input = ref(null)
onMounted(() => { input.value.focus() })
</script>
<template>
<input ref="input" />
</template>
- 模板引用仅在挂载后可用,挂载前为
null - 使用
watchEffect处理可能为null的情况
组件上的 ref
<script setup> 组件默认是私有的,父组件无法访问子组件内部。需用 defineExpose 显式暴露:
<!-- 子组件 Child.vue -->
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({ a, b }) // 必须在任何 await 之前调用
</script>
<!-- 父组件 -->
<script setup>
import { useTemplateRef, onMounted } from 'vue'
import Child from './Child.vue'
const child = useTemplateRef('child')
onMounted(() => { console.log(child.value) }) // { a: 1, b: Ref(2) }
</script>
<template>
<Child ref="child" />
</template>
v-for 中的模板引用 (Vue 3.5+)
<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'
const list = ref([/* ... */])
const itemRefs = useTemplateRef('items')
onMounted(() => console.log(itemRefs.value))
</script>
<template>
<ul>
<li v-for="item in list" ref="items">{{ item }}</li>
</ul>
</template>
v-for ref 数组不保证与源数组顺序一致。
函数模板引用
<input :ref="(el) => { /* 将 el 赋值给变量 */ }">
元素卸载时参数为 null。使用动态 :ref 绑定(非静态 ref)。
十三、生命周期钩子
<script setup>
import { onMounted, onUpdated, onUnmounted } from 'vue'
onMounted(() => {
console.log('组件已挂载') // 适合:访问 DOM、发起初始化请求
})
onUpdated(() => {
console.log('组件已更新')
})
onUnmounted(() => {
console.log('组件已卸载') // 适合:清除定时器、取消订阅
})
</script>
完整生命周期顺序:
setup → onBeforeMount → onMounted → onBeforeUpdate → onUpdated → onBeforeUnmount → onUnmounted
重要规则: 生命周期钩子必须同步注册。异步注册(如 setTimeout 内)不会生效,因为当前组件实例已丢失。只要调用栈是同步且源于 setup(),在外部函数中调用 onMounted() 是允许的。
十四、组件基础
组件允许我们将 UI 划分为独立的、可重用的部分。组件组织为层层嵌套的树状结构。
定义组件
使用单文件组件 .vue:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
不使用构建步骤时,以包含选项的 JS 对象定义:
export default {
data() { return { count: 0 } },
template: `<button @click="count++">{{ count }}</button>`
}
使用组件
<script setup> 中导入的组件直接可用,无需注册:
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>
选项式 API 需在 components 选项中注册。
命名规范:
-
SFC 中推荐 PascalCase:
<ButtonCounter /> -
DOM 内模板必须用 kebab-case:
<button-counter></button-counter> -
每使用一次组件就创建一个新实例,各实例维护各自的状态
-
全局注册:
app.component('ButtonCounter', ButtonCounter)— 任何组件中可直接使用
传递 Props
Props 是父组件向子组件传递数据的机制:
<!-- 子组件 BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>
<template>
<h4>{{ title }}</h4>
</template>
<!-- 父组件 -->
<BlogPost v-for="post in posts" :key="post.id" :title="post.title" />
defineProps是编译宏,仅<script setup>可用,无需导入- 可接收返回值:
const props = defineProps(['title'])
监听事件
子组件通过 emit 向父组件通信:
<!-- 子组件 -->
<script setup>
const emit = defineEmits(['enlarge-text'])
</script>
<template>
<button @click="emit('enlarge-text')">Enlarge text</button>
</template>
<!-- 父组件 -->
<BlogPost @enlarge-text="postFontSize += 0.1" />
- 声明事件可让 Vue 避免将其作为原生事件应用到子组件根元素
通过插槽分配内容
使用 <slot> 作为占位符,父组件传递的内容渲染在此处:
<!-- AlertBox.vue -->
<template>
<div class="alert-box">
<strong>This is an Error for Demo Purposes</strong>
<slot />
</div>
</template>
<!-- 使用 -->
<AlertBox>
Something bad happened.
</AlertBox>
动态组件
使用 <component :is=""> 切换组件:
<component :is="currentTab"></component>
:is 的值可以是注册的组件名字符串或导入的组件对象。切换时被替换的组件会被卸载,可用 <KeepAlive> 保持存活状态。
DOM 内模板解析注意事项
仅适用于直接在 DOM 中编写模板(SFC 不受限):
| 注意事项 | 说明 |
|---|---|
| 大小写 | PascalCase 组件名、camelCase prop/事件名 → 必须转 kebab-case |
| 闭合标签 | 必须显式写关闭标签 <my-component></my-component>,不能自闭合 |
| 元素位置 | <ul> 只能放 <li> 等。解决方案:<tr is="vue:blog-post-row">(注意 vue: 前缀) |
更多推荐

所有评论(0)