Vue 常用技术知识全景:从响应式到组件通信的系统理解

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:跨层级传递依赖

provideinject 适合跨层级传递主题、配置、表单上下文或服务对象。

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 项目中状态可以分层:

  1. 组件内部状态:只影响单个组件。
  2. 父子通信状态:父组件持有,子组件通过 props/emit 读写。
  3. 跨层级上下文:provide/inject。
  4. 全局业务状态:Pinia 等状态库。
flowchart TD A[&#34;状态影响范围&#34;] --> B{&#34;只影响一个组件?&#34;} B -->|是| C[&#34;放在组件内部&#34;] B -->|否| D{&#34;兄弟组件共享?&#34;} D -->|是| E[&#34;提升到共同父组件&#34;] D -->|否| F{&#34;跨层级稳定依赖?&#34;} F -->|是| G[&#34;provide / inject&#34;] F -->|否| H[&#34;Pinia 或外部状态管理&#34;]

边界与失效场景:不要把所有状态都放进全局 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-ifv-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 项目时,优先检查这些位置:

  1. Vue Devtools:组件树、props、state、事件。
  2. console.log:放在事件处理函数、watch 回调、生命周期函数入口。
  3. Network 面板:接口请求是否重复、参数是否正确。
  4. Performance 面板:交互后是否有长任务。
  5. 控制台 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>templatestyle scoped 的基本结构。
  • 能区分 refreactivecomputedwatch
  • 能解释模板中 ref 自动解包和脚本中 .value 的差异。
  • 能写出 props/emit、v-model、slot、provide/inject。
  • 能说明 v-ifv-showv-for:key 的使用边界。
  • 能把副作用放到 watch、生命周期或事件处理函数里。
  • 能把复用逻辑抽成 composable。
  • 能按组件状态、URL 状态、全局状态划分数据来源。
  • 能使用 Vue Devtools、console、Network、Performance 定位问题。

总结

Vue 的常用技术知识可以用一条主线串起来:模板描述 UI,响应式状态驱动更新,computed 负责派生值,watch 和生命周期负责副作用,props/emit 保持父子组件单向数据流,slot 和 provide/inject 解决更灵活的组合与跨层级传递。

写 Vue 时,先让状态来源清晰、组件边界清楚,再处理复用、跨层级通信和性能问题。

面试回答也按这条线展开:先讲定义,再讲机制,然后给最小代码,最后补充边界场景。

参考资料

相关推荐
feiyu_gao1 小时前
一个人 + AI:246 commits 做出设计系统 CLI 的故事
前端·ai编程·交互设计
奶油mm1 小时前
从 0 到 1 搭建高可用 Redis Cluster:踩坑、优化与生产实践
前端
掘金安东尼1 小时前
Agent Loop 深度调研:把决定权交给模型的一次换代,为什么发生在现在
前端
亿元程序员2 小时前
Cocos视频拼图,终于支持微信小游戏了!
前端
JarvanMo2 小时前
Flutter 的默认颜色
前端
IT_陈寒2 小时前
Vite打包时踩的坑:静态资源为啥突然404了?
前端·人工智能·后端
神奇的程序员11 小时前
我的软件冲进苹果商店下载榜前 50 了
前端
阳光是sunny12 小时前
别再被 worktree 绕晕了!AI 编程时代你必须掌握的 Git 隔离神器
前端·人工智能·后端
万少13 小时前
万少的博客 - 技术分享与解决方案
前端·javascript·后端