Vue 常用技术知识全景:从响应式到组件通信的系统理解
你写 Vue 时经常会遇到这些问题:为什么模板里不用写 .value,脚本里却要写?为什么 computed 有缓存,而 watch 适合处理副作用?为什么子组件不能直接改父组件传来的 props?
这些问题不是零散 API,而是 Vue 的响应式系统、组件模型和单向数据流在真实业务中的表现。
如果能把模板编译、响应式依赖收集、组件通信、生命周期和副作用管理串起来,Vue 项目会更容易拆分、调试和维护。
本文面向已经会写基础 Vue 单文件组件、想系统整理 Vue 常用知识的前端开发者。示例以 Vue 3、单文件组件和 Composition API 写法为主;Options API 仍然是 Vue 支持的写法,本文会在关键位置说明差异。
1. Vue 的核心模型:声明式渲染 + 响应式更新
Vue 的第一层心智模型是:用模板声明 UI,用响应式状态驱动 UI 更新。
vue
<script setup>
import { ref } from "vue";
const count = ref(0);
function increment() {
count.value += 1;
}
</script>
<template>
<button type="button" @click="increment">
Count: {{ count }}
</button>
</template>
在 <script setup> 中,count 是一个 ref,需要通过 count.value 读写。模板中 Vue 会自动解包 ref,所以可以直接写 {{ count }}。
边界与失效场景:模板自动解包不等于 JavaScript 中也能省略 .value。在 <script setup>、普通函数、composable 中读写 ref,都要使用 .value。
面试回答模板:
Vue 通过响应式系统追踪状态读取和写入。组件渲染时读取响应式数据,数据变化后 Vue 会调度组件重新渲染,让模板和状态保持同步。
2. 单文件组件:把模板、逻辑和样式组织在一起
Vue 单文件组件通常由三部分组成:
<script setup>:组件逻辑。<template>:组件模板。<style scoped>:组件样式。
vue
<script setup>
const title = "Vue Knowledge";
</script>
<template>
<article class="card">
<h1>{{ title }}</h1>
</article>
</template>
<style scoped>
.card {
padding: 16px;
border: 1px solid #ddd;
}
</style>
<script setup> 是 Composition API 在单文件组件中的常用写法,顶层变量和函数可以直接在模板中使用。
边界与失效场景:<style scoped> 会限制样式作用范围,但它不是 Shadow DOM。深层子组件、第三方组件内部结构和全局样式仍需要按 Vue 样式规则单独处理。
3. 模板语法:表达 UI,而不是堆逻辑
Vue 模板支持文本插值、属性绑定、事件监听、条件渲染和列表渲染。
vue
<script setup>
import { ref } from "vue";
const isLoggedIn = ref(true);
const user = ref({
id: "u1",
name: "Ada",
});
function logout() {
isLoggedIn.value = false;
}
</script>
<template>
<section>
<p v-if="isLoggedIn">Hello, {{ user.name }}</p>
<p v-else>Please log in.</p>
<button type="button" :disabled="!isLoggedIn" @click="logout">
Logout
</button>
</section>
</template>
常用语法:
{{ message }}:文本插值。:disabled="value":属性绑定,等价于v-bind:disabled。@click="handler":事件监听,等价于v-on:click。v-if / v-else-if / v-else:条件渲染。v-for:列表渲染。
边界与失效场景:模板里应放轻量表达式。复杂业务逻辑放到函数、computed 或 composable 中,模板负责表达结构和绑定关系。
4. ref 与 reactive:响应式状态的两种常见入口
ref 适合包装任意值,尤其是基本类型。reactive 适合包装对象。
vue
<script setup>
import { reactive, ref } from "vue";
const keyword = ref("");
const form = reactive({
name: "",
city: "",
});
function resetForm() {
keyword.value = "";
form.name = "";
form.city = "";
}
</script>
<template>
<label>
Keyword
<input v-model="keyword" />
</label>
<label>
Name
<input v-model="form.name" />
</label>
<button type="button" @click="resetForm">Reset</button>
</template>
使用建议:
- 基本类型状态:优先
ref。 - 对象表单或一组强相关字段:可以使用
reactive。 - 需要整体替换对象时:使用
ref({ ... })更直接。
边界与失效场景:reactive 返回的是响应式代理对象,不要把它解构后直接使用普通变量,否则会丢失响应式连接。需要解构时使用 toRefs() 或保持对象访问方式。
5. computed:从状态派生状态
computed 适合描述"由已有响应式数据计算出的值"。它会基于依赖缓存结果,依赖没有变化时不会重复计算。
vue
<script setup>
import { computed, ref } from "vue";
const firstName = ref("Ada");
const lastName = ref("Lovelace");
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
</script>
<template>
<p>{{ fullName }}</p>
</template>
边界与失效场景:computed 应保持纯粹,不要在里面发请求、写 DOM、修改其他状态。需要处理副作用时使用事件处理函数或 watch。
可写 computed:
vue
<script setup>
import { computed, ref } from "vue";
const firstName = ref("Ada");
const lastName = ref("Lovelace");
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(nextFullName) {
const [nextFirstName = "", nextLastName = ""] = nextFullName.split(" ");
firstName.value = nextFirstName;
lastName.value = nextLastName;
},
});
</script>
<template>
<input v-model="fullName" />
</template>
边界与失效场景:可写 computed 适合把一个展示值映射回多个状态字段。解析规则要明确处理空字符串、多个空格和缺失字段,否则用户输入会得到不符合预期的拆分结果。
6. watch 与 watchEffect:处理响应式副作用
watch 适合监听明确的数据源,并在变化后执行副作用。
vue
<script setup>
import { ref, watch } from "vue";
const keyword = ref("");
const result = ref("");
const errorMessage = ref("");
watch(keyword, async (nextKeyword) => {
const normalizedKeyword = nextKeyword.trim();
if (normalizedKeyword.length === 0) {
result.value = "";
return;
}
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(normalizedKeyword)}`);
const data = await response.json();
result.value = data?.title ?? "No result";
errorMessage.value = "";
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : "Unknown error";
}
});
</script>
<template>
<input v-model="keyword" placeholder="Search" />
<p v-if="errorMessage">{{ errorMessage }}</p>
<p v-else>{{ result }}</p>
</template>
边界与失效场景:这个示例依赖浏览器 fetch 和本地 /api/search 接口。真实项目中还需要处理请求竞态、取消过期请求、加载态和接口结构校验。
watchEffect 会自动追踪回调里读取到的响应式依赖:
vue
<script setup>
import { ref, watchEffect } from "vue";
const width = ref(window.innerWidth);
watchEffect((onCleanup) => {
function handleResize() {
width.value = window.innerWidth;
}
window.addEventListener("resize", handleResize);
onCleanup(() => {
window.removeEventListener("resize", handleResize);
});
});
</script>
<template>
<p>Window width: {{ width }}</p>
</template>
边界与失效场景:这段代码只适合浏览器环境。SSR 场景中不能在服务端直接读取 window,应把浏览器 API 放到 onMounted 或仅客户端执行的逻辑里。
7. 生命周期:在正确时间访问 DOM 和外部资源
Composition API 中常见生命周期函数包括:
onMounted:组件挂载到 DOM 后执行。onUpdated:组件因响应式状态变化更新 DOM 后执行。onUnmounted:组件卸载后执行清理。
vue
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
const buttonRef = ref(null);
function handleKeydown(event) {
if (event.key === "Escape") {
console.log("escape pressed");
}
}
onMounted(() => {
buttonRef.value?.focus();
window.addEventListener("keydown", handleKeydown);
});
onUnmounted(() => {
window.removeEventListener("keydown", handleKeydown);
});
</script>
<template>
<button ref="buttonRef" type="button">Focusable button</button>
</template>
边界与失效场景:DOM ref 在挂载前是 null。事件监听、timer、订阅和第三方实例要在卸载时清理,否则页面切换后仍会占用资源或触发旧逻辑。
8. 组件通信:props 向下,事件向上
Vue 组件通信的基础模式是:父组件通过 props 传数据,子组件通过 emit 发事件。
vue
<!-- ParentPanel.vue -->
<script setup>
import { ref } from "vue";
import CounterButton from "./CounterButton.vue";
const count = ref(0);
function increment() {
count.value += 1;
}
</script>
<template>
<CounterButton :count="count" @increment="increment" />
</template>
vue
<!-- CounterButton.vue -->
<script setup>
defineProps({
count: {
type: Number,
required: true,
},
});
const emit = defineEmits(["increment"]);
</script>
<template>
<button type="button" @click="emit('increment')">
Count: {{ count }}
</button>
</template>
边界与失效场景:子组件不要直接修改 props。props 是父组件传下来的输入,子组件要表达修改意图时,通过事件通知父组件。
9. v-model:把值和更新事件封装成组件约定
在自定义组件上使用 v-model,本质上是传入 modelValue,监听 update:modelValue。
vue
<!-- TextField.vue -->
<script setup>
defineProps({
modelValue: {
type: String,
required: true,
},
});
const emit = defineEmits(["update:modelValue"]);
</script>
<template>
<input
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
vue
<!-- App.vue -->
<script setup>
import { ref } from "vue";
import TextField from "./TextField.vue";
const name = ref("");
</script>
<template>
<TextField v-model="name" />
<p>Hello, {{ name }}</p>
</template>
边界与失效场景:自定义组件里的 v-model 不应该直接修改 prop。应该通过 emit("update:modelValue", nextValue) 把新值交给父组件。
10. 列表渲染与 key:给每一项稳定身份
v-for 渲染列表时,应给每一项提供稳定 key。
vue
<script setup>
const todos = [
{ id: "t1", text: "Read Vue docs" },
{ id: "t2", text: "Write notes" },
];
</script>
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
</li>
</ul>
</template>
边界与失效场景:不要在会增删、排序、过滤的列表中使用数组下标作为 key。下标代表位置,不代表数据身份;顺序变化后,组件局部状态会跟着位置走。
11. slot:让父组件决定一部分 UI
slot 用于把组件的一部分结构交给调用方定义。
vue
<!-- Card.vue -->
<template>
<article class="card">
<header>
<slot name="title" />
</header>
<main>
<slot />
</main>
</article>
</template>
vue
<!-- App.vue -->
<script setup>
import Card from "./Card.vue";
</script>
<template>
<Card>
<template #title>
<h2>Vue Slot</h2>
</template>
<p>Slot lets parent components provide content.</p>
</Card>
</template>
边界与失效场景:slot 适合复用结构,不适合隐藏过多业务状态。子组件需要向 slot 暴露数据时,使用作用域插槽。
作用域插槽:
vue
<!-- UserList.vue -->
<script setup>
defineProps({
users: {
type: Array,
required: true,
},
});
</script>
<template>
<ul>
<li v-for="user in users" :key="user.id">
<slot name="item" :user="user" />
</li>
</ul>
</template>
边界与失效场景:作用域插槽会让父组件知道更多子组件数据结构。公共组件设计时,应保持暴露数据简洁稳定。
12. provide 与 inject:跨层级传递依赖
provide 和 inject 适合跨层级传递主题、配置、表单上下文或服务对象。
vue
<!-- App.vue -->
<script setup>
import { provide, ref } from "vue";
import Toolbar from "./Toolbar.vue";
const theme = ref("dark");
provide("theme", theme);
</script>
<template>
<Toolbar />
</template>
vue
<!-- Toolbar.vue -->
<script setup>
import { inject } from "vue";
const theme = inject("theme", "light");
</script>
<template>
<div :data-theme="theme">Toolbar theme: {{ theme }}</div>
</template>
边界与失效场景:inject 会让依赖关系不如 props 明显。对局部父子组件通信,优先 props 和 emit;对跨层级稳定依赖,再使用 provide/inject。
13. composable:复用状态逻辑
composable 是以 use 开头的函数,用来复用响应式逻辑。
javascript
// useOnlineStatus.js
import { onMounted, onUnmounted, ref } from "vue";
export function useOnlineStatus() {
const isOnline = ref(navigator.onLine);
function handleOnline() {
isOnline.value = true;
}
function handleOffline() {
isOnline.value = false;
}
onMounted(() => {
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
});
onUnmounted(() => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
});
return {
isOnline,
};
}
vue
<script setup>
import { useOnlineStatus } from "./useOnlineStatus";
const { isOnline } = useOnlineStatus();
</script>
<template>
<p>{{ isOnline ? "Online" : "Offline" }}</p>
</template>
边界与失效场景:每次调用 composable 都会创建独立状态,除非 composable 内部显式使用模块级单例。需要共享状态时,要明确设计共享范围。
14. 状态管理:组件状态、provide/inject、Pinia 各有边界
Vue 项目中状态可以分层:
- 组件内部状态:只影响单个组件。
- 父子通信状态:父组件持有,子组件通过 props/emit 读写。
- 跨层级上下文:provide/inject。
- 全局业务状态:Pinia 等状态库。
边界与失效场景:不要把所有状态都放进全局 store。局部状态全局化会让组件复用、测试和删除功能变得更重。
15. 路由与页面状态:区分 URL 状态和组件状态
Vue Router 常用于管理页面路由。一般工程里可以按状态来源拆分:
- URL 中的状态:路由 path、query、params。
- 页面临时状态:当前弹窗、局部输入框、tab 展开状态。
- 业务共享状态:用户信息、权限、购物车、全局配置。
vue
<script setup>
import { computed } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const keyword = computed(() => {
return String(route.query.keyword ?? "").trim();
});
</script>
<template>
<p>Keyword from URL: {{ keyword }}</p>
</template>
边界与失效场景:URL 是可分享、可刷新恢复的状态来源。只属于当前组件交互的短暂状态,不需要都写进 URL。
16. 性能与渲染:先定位,再优化
Vue 的响应式系统会自动追踪依赖,但组件设计仍然影响渲染成本。
常见检查点:
- 大列表是否分页、虚拟滚动或懒加载。
computed是否替代了重复模板计算。- 组件 props 是否过大或变化过于频繁。
v-if和v-show是否按切换频率选择。- 列表
key是否稳定。
vue
<script setup>
import { computed, ref } from "vue";
const keyword = ref("");
const products = ref([
{ id: "p1", name: "Keyboard" },
{ id: "p2", name: "Mouse" },
]);
const filteredProducts = computed(() => {
const normalizedKeyword = keyword.value.trim().toLowerCase();
if (normalizedKeyword.length === 0) {
return products.value;
}
return products.value.filter((product) =>
product.name.toLowerCase().includes(normalizedKeyword)
);
});
</script>
<template>
<input v-model="keyword" />
<ul>
<li v-for="product in filteredProducts" :key="product.id">
{{ product.name }}
</li>
</ul>
</template>
边界与失效场景:computed 适合缓存依赖明确的派生值。接口请求、DOM 操作、日志上报等副作用不要放入 computed。
17. 调试 Vue:从响应式依赖和组件树入手
调试 Vue 项目时,优先检查这些位置:
- Vue Devtools:组件树、props、state、事件。
console.log:放在事件处理函数、watch 回调、生命周期函数入口。- Network 面板:接口请求是否重复、参数是否正确。
- Performance 面板:交互后是否有长任务。
- 控制台 warning:props 类型、key、组件注册、模板编译错误。
vue
<script setup>
import { watch } from "vue";
const props = defineProps({
userId: {
type: String,
required: true,
},
});
watch(
() => props.userId,
(nextUserId, previousUserId) => {
console.log("userId changed:", {
previousUserId,
nextUserId,
});
}
);
</script>
<template>
<p>User: {{ userId }}</p>
</template>
边界与失效场景:调试日志要放在能说明状态流转的位置。不要在模板里塞复杂表达式来调试,模板应保持可读。
18. Vue 常见面试回答模板
| 问题 | 回答重点 |
|---|---|
| Vue 的响应式原理怎么理解? | 组件渲染读取响应式数据,Vue 追踪依赖;数据变化后触发相关组件更新。 |
| ref 和 reactive 区别? | ref 包装任意值并通过 .value 访问;reactive 包装对象代理。 |
| computed 和 watch 区别? | computed 用于派生值,带缓存;watch 用于监听变化后执行副作用。 |
| 为什么不能直接改 props? | props 是父组件传入的单向输入,子组件应通过 emit 通知父组件更新。 |
| v-if 和 v-show 区别? | v-if 控制是否创建节点;v-show 控制 display,适合频繁切换显示状态。 |
| 为什么 v-for 要写 key? | key 表示列表项身份,帮助 Vue 在增删排序时正确复用节点和组件状态。 |
| composable 解决什么问题? | 复用响应式状态逻辑,而不是复用 UI 结构。 |
| provide/inject 适合什么场景? | 跨层级传递稳定依赖,例如主题、表单上下文、配置或服务对象。 |
19. 最后复习清单
准备 Vue 技术知识或面试时,可以按这份清单复习:
- 能说明 Vue 的声明式渲染和响应式更新模型。
- 能写出
<script setup>、template、style scoped的基本结构。 - 能区分
ref、reactive、computed、watch。 - 能解释模板中 ref 自动解包和脚本中
.value的差异。 - 能写出 props/emit、
v-model、slot、provide/inject。 - 能说明
v-if、v-show、v-for、:key的使用边界。 - 能把副作用放到 watch、生命周期或事件处理函数里。
- 能把复用逻辑抽成 composable。
- 能按组件状态、URL 状态、全局状态划分数据来源。
- 能使用 Vue Devtools、console、Network、Performance 定位问题。
总结
Vue 的常用技术知识可以用一条主线串起来:模板描述 UI,响应式状态驱动更新,computed 负责派生值,watch 和生命周期负责副作用,props/emit 保持父子组件单向数据流,slot 和 provide/inject 解决更灵活的组合与跨层级传递。
写 Vue 时,先让状态来源清晰、组件边界清楚,再处理复用、跨层级通信和性能问题。
面试回答也按这条线展开:先讲定义,再讲机制,然后给最小代码,最后补充边界场景。