前端框架 · 大学二年级

掌握 Vue 3:渐进式 JavaScript 框架

从响应式原理、模板语法到组件化、组合式 API,再到 Vue Router 路由、Pinia 状态管理,最后用 Vite 工具链完成 3 个真实项目。 本教程与本系列前序课程(C → C++ → Java → Python → SQL → JavaScript → HTML → CSS)保持一致的节奏与难度梯度。

🟢 29 章正文 📦 3 个综合项目 ❓ 12 道课堂训练 🎯 Composition API 🛠 Vite + Pinia + Router
模板 + 响应式 组件基础 组合式 API Router + Pinia Vite 构建 真实项目

学习指南:Vue 是什么、怎么学、学到什么

Vue 3 是当前最主流的渐进式 JavaScript 框架。它用声明式渲染 + 响应式数据 + 组件化三大特性,让你能用更少代码写出可维护的前端应用。

0.1 Vue 三大特性

💡 三件套
声明式渲染  · 用模板描述"长什么样"
响应式数据  · 数据变化自动更新界面
组件化     · 把页面拆成一个个可复用单元

0.2 Vue 2 vs Vue 3

维度Vue 2Vue 3
语法Options APIComposition API(推荐)
响应式Object.definePropertyProxy(更强大)
TypeScript支持一般原生友好
状态管理VuexPinia(官方推荐)
构建工具Vue CLIVite

0.3 路线

  1. 模板语法 + 响应式数据 + 事件
  2. 组件化(props / emit / slot)
  3. 组合式 API(setup / ref / computed / watch)
  4. 生态:Vue Router + Pinia
  5. 工程化:Vite + 单文件组件 + 部署
前置要求:需要掌握 HTML + CSS + JavaScript(尤其是 ES6+ 语法)。

快速起步 ⭐⭐⭐

1.1 两种启动方式

方式适用命令
CDN 引入学习、demo直接在 HTML 加 <script>
Vite + npm真实项目npm create vue@latest

1.2 CDN Hello World

HTML
<div id="app">{{ msg }}</div>

<script type="module">
    import { createApp, ref } from "https://unpkg.com/vue@3/dist/vue.esm-browser.js";

    createApp({
        setup() {
            const msg = ref("你好,Vue 3!");
            return { msg };
        }
    }).mount("#app");
</script>

1.3 用 Vite 脚手架

终端
npm create vue@latest
# 按提示选择 TypeScript / Router / Pinia / 测试
cd my-app
npm install
npm run dev   # 启动 localhost:5173

模板语法 ⭐⭐⭐

2.1 插值

Vue 模板
<template>
    <p>{{ msg }}</p>
    <p>{{ n + 1 }}</p>
    <p>{{ ok ? "YES" : "NO" }}</p>
    <p>{{ formatDate(d) }}</p>
</template>

2.2 常见指令

指令作用
v-text更新文本
v-html解析 HTML(注意 XSS)
v-show切换 CSS display
v-if / v-else条件渲染(条件为 false 时销毁)
v-for列表渲染(必须加 :key
v-on / @事件监听
v-bind / :属性绑定
v-model双向绑定(表单)
Vue 模板
<a :href="url" :title="tip">链接</a>
<img :src="src" alt="">

<button @click="onClick">点击</button>
<input @keyup.enter="submit">

<p v-if="seen">看到了</p>
<p v-else>没看到</p>

<li v-for="item in items" :key="item.id">{{ item.name }}</li>

响应式数据 ref / reactive ⭐⭐⭐

3.1 ref:让任意类型变响应式

JavaScript (Vue)
import { ref } from "vue";

const count = ref(0);
console.log(count.value);    // 0(脚本里要 .value)
count.value++;             // 修改

// 模板里自动解包,不需要 .value
<template>
    <button @click="count++">{{ count }}</button>
</template>

3.2 reactive:对象式响应式

JavaScript (Vue)
import { reactive } from "vue";

const state = reactive({
    name: "张三",
    age:  18,
    hobbies: ["代码", "音乐"]
});

state.age = 19;                       // 直接改属性
state.hobbies.push("阅读");       // 数组方法也响应

3.3 ref vs reactive 之选

refreactive
任何值(基本/对象)只用于对象
用 .value 访问直接访问属性
现代风格推荐深度嵌套数据方便
推荐:默认用 ref,对象大且深嵌套时用 reactive

计算属性 computed ⭐⭐

Vue
import { ref, computed } from "vue";

const firstName = ref("三");
const lastName  = ref("张");

const fullName = computed(() => lastName.value + firstName.value);

// fullName 是只读的
<p>{{ fullName }}</p>

computed vs method

computedmethod
有缓存(依赖不变不重算)每次重算
适合"由其他数据派生"的值适合事件/动作

侦听器 watch / watchEffect ⭐⭐⭐

Vue
import { ref, watch, watchEffect } from "vue";

const id = ref(1);

// 监听一个 ref
watch(id, (newV, oldV) => {
    console.log(`id 从 ${oldV}${newV}`);
});

// 立即执行 + 自动监听
watchEffect(() => {
    console.log("id = ", id.value);
    // 自动收集依赖
});

// 监听对象里的属性
const user = reactive({ name: "Alice", age: 18 });
watch(() => user.age, (n, o) => console.log(n));

watch 选项速查

选项作用
immediate: true立即执行一次回调
deep: true深度监听对象内部
flush: "post"DOM 更新后再触发

class 与 style 绑定 ⭐⭐

Vue 模板
<div :class="{ active: isActive, 'text-danger': hasError }">对象式</div>
<div :class="[a, b]">数组式</div>
<div :style="{ color: c, fontSize: size + 'px' }">行内</div>

条件渲染与列表渲染 ⭐⭐⭐

7.1 v-if vs v-show

v-ifv-show
false 时不创建始终创建,CSS 切换
首次渲染慢,切换开销小首次渲染快,切换开销大
适合不常切换适合频繁切换

7.2 v-for 必须 :key

Vue
<ul>
    <li v-for="(user, i) in users"
        :key="user.id">
        {{ i }} - {{ user.name }}
    </li>
</ul>
⚠️ key 别用 index

:key="i" 在数据顺序变化时会导致 DOM 错误复用,应使用稳定 id

事件处理 ⭐⭐

Vue
<button @click="count++">+</button>
<button @click="say('hi')">问候</button>

// 事件修饰符
<form @submit.prevent="onSubmit">      // e.preventDefault()
<div   @click.stop="onClick">         // e.stopPropagation()
<div   @click.once="onClick">        // 只触发一次
<input @keyup.enter="submit">       // 回车键
<input @click.ctrl="onClick">        // Ctrl+点击

表单输入绑定 v-model ⭐⭐⭐

9.1 各种控件

Vue 模板
<input     v-model="name"          />        /* 文本 */
<input     type="checkbox" v-model="ok"/>  /* 复选框(boolean) */
<input     type="checkbox" value="vue"
                                  v-model="arr"/>  /* 数组(多选) */
<input     type="radio" v-model="pick" />/* 单选 */
<select   v-model="city">...</select>           /* 下拉 */
<textarea v-model="msg"></textarea>          /* 多行 */

// 修饰符
<input v-model.lazy="name">            // change 时同步
<input v-model.number="age">          // 转数字
<input v-model.trim="name">           // 去空白

Vue 实例生命周期 ⭐⭐

setup() onBeforeMount onMounted ✓ 运行中(响应式) onBeforeUpdate onUpdated onBeforeUnmount onUnmounted 常用:onMounted(发请求) / onUnmounted(清理定时器) 生命周期贯穿"创建 → 挂载 → 更新 → 卸载"
Vue
import { onMounted, onUnmounted, ref } from "vue";

setup() {
    const list = ref([]);

    onMounted(async () => {
        const r = await fetch("/api/list");
        list.value = await r.json();
    });

    let timer;
    onMounted(() => timer = setInterval(tick, 1000));
    onUnmounted(() => clearInterval(timer));

    return { list };
}

单文件组件 SFC ⭐⭐⭐

真实项目用 .vue 文件,每个组件内含三段:<template> + <script> + <style>

App.vue
<template>
    <div class="box">
        <h1>{{ msg }}</h1>
        <button @click="inc">{{ count }}</button>
    </div>
</template>

<script setup>
    import { ref } from "vue";
    const msg   = ref("Hello Vue");
    const count = ref(0);
    const inc   = () => count.value++;
</script>

<style scoped>
    .box { padding: 20px; }
    .box h1 { color: #42B883; }
</style>
scoped 让样式只作用于当前组件,避免样式污染。

组件基础 ⭐⭐⭐

12.1 注册与使用

MyButton.vue
<template>
    <button :disabled="disabled" class="btn" @click="emit('click')">
        <slot />
    </button>
</template>

<script setup>
    defineProps({ disabled: Boolean });
    const emit = defineEmits(["click"]);
</script>

<style scoped>
    .btn {
        background: #42B883; color: #fff;
        border: none; border-radius: 6px;
        padding: 8px 16px;
    }
</style>

12.2 在父组件使用

App.vue
<template>
    <MyButton @click="onClick">点我</MyButton>
    <MyButton disabled>禁用</MyButton>
</template>

<script setup>
    import MyButton from "./components/MyButton.vue";
    const onClick = () => alert("click");
</script>

12.3 全局注册

main.js
import { createApp } from "vue";
import MyButton from "./components/MyButton.vue";

const app = createApp({});
app.component("MyButton", MyButton);
app.mount("#app");

组件通信:props / emit / slot ⭐⭐⭐

13.1 父传子:defineProps

Child.vue
<script setup>
    const props = defineProps({
        title: { type: String, required: true },
        count: { type: Number, default: 0 },
        tags:  { type: Array, default: () => [] }
    });
</script>

<template>
    <h2>{{ props.title }} ({{ props.count }})</h2>
</template>

13.2 子传父:defineEmits

Child.vue
<template>
    <button @click="send">发给爸爸</button>
</template>

<script setup>
    const emit = defineEmits(["greet"]);
    const send = () => emit("greet", "hi from child");
</script>

// 父组件
<Child @greet="onGreet" />
const onGreet = (msg) => console.log(msg);

13.3 插槽 slot

Card.vue
<template>
    <div class="card">
        <div class="header"><slot name="header" /></div>
        <div class="body"><slot /></div>
        <div class="footer"><slot name="footer" /></div>
    </div>
</template>

<Card>
  <template #header>标题</template>
  <p>默认插槽内容</p>
  <template #footer><button>确定</button></template>
</Card>

组件通信高级:v-model / provide-inject ⭐⭐

14.1 父子 v-model

自定义 v-model 组件
<script setup>
    defineProps(["modelValue"]);
    const emit = defineEmits(["update:modelValue"]);
    const input = (e) => emit("update:modelValue", e.target.value);
</script>

<template>
    <input :value="modelValue" @input="input"/>
</template>

// 父组件
<MyInput v-model="text" />

14.2 provide / inject(跨层级)

<script setup>
    import { provide, ref } from "vue";
    const user = ref({ name: "Alice" });
    provide("user", user);
</script>

// 孙组件
<script setup>
    import { inject } from "vue";
    const user = inject("user");
</script>

组件设计原则

✅ 良好组件
  • 职责单一(一个组件做一件事)
  • 可独立复用,依赖低
  • Props 通过 defineProps 显式声明类型
  • 状态提升到共同父组件
  • 跨层级通信用 provide/inject 或 状态管理库
❌ 反模式
  • 巨型组件(> 500 行):拆
  • 深嵌套 props:考虑 provide/inject
  • 业务逻辑混在模板里:抽出为 setup 函数
  • 父子双向直接改 props:永远不允许

组合式 API 入门 setup ⭐⭐⭐

setup 是 Vue 3 推荐的写法。所有逻辑都在这里组织,而非 Options API 的 data/methods/computed 分块。这样更易抽离复用。

16.1 setup 两种风格

Vue SFC
<script setup>
    // 顶层变量、函数自动暴露给模板
    import { ref, computed } from "vue";

    const n = ref(0);
    const double = computed(() => n.value * 2);

    function inc() { n.value++; }
</script>

<template>
    <button @click="inc">{{ n }} × 2 = {{ double }}</button>
</template>

16.2 抽离逻辑到 composable

useMouse.js
import { ref, onMounted, onUnmounted } from "vue";

export function useMouse() {
    const x = ref(0), y = ref(0);

    function upd(e) {
        x.value = e.clientX;
        y.value = e.clientY;
    }

    onMounted(() => window.addEventListener("mousemove", upd));
    onUnmounted(() => window.removeEventListener("mousemove", upd));

    return { x, y };
}

// 任何组件都可以:
// import { useMouse } from "./useMouse";
// const { x, y } = useMouse();

深入:ref vs reactive 的内部原理

17.1 ref 的内部

示意
// ref 内部大致是:
class RefImpl {
    constructor(value) {
        this._value = toReactive(value);
    }
    get value() {
        track(this);          // 依赖收集
        return this._value;
    }
    set value(v) {
        this._value = toReactive(v);
        trigger(this);         // 通知更新
    }
}

17.2 模板自动解包

<p>{{ count }}</p> 实际编译为 count.value,但在子组件或模板中会自动解包。

reactive 解包
const obj = reactive({ a: 1 });
const wrapped = { x: obj };
wrapped.x.a;       // ❌ 不响应(被包了一层普通对象)
wrapped.x.a = 2;  // ❌ 不会触发更新

// 解决:把 reactive 重新赋值给响应式
wrapped.x = reactive(wrapped.x);
wrapped.x.a = 2;  // ✅ 响应

ref 在模板与 DOM 的用法 ⭐⭐

Vue SFC
<script setup>
    import { ref } from "vue";
    const inp = ref(null);

    onMounted(() => inp.value.focus());
</script>

<template>
    <input ref="inp" type="text">
</template>

Vue Router 路由 ⭐⭐⭐

19.1 安装与配置

终端
npm install vue-router@4

19.2 路由配置

router/index.js
import { createRouter, createWebHistory } from "vue-router";
import Home    from "../views/Home.vue";
import About   from "../views/About.vue";
import NotFound from "../views/NotFound.vue";

export default createRouter({
    history: createWebHistory(),
    routes: [
        { path: "/",          component: Home,    name: "home" },
        { path: "/about",     component: About,   name: "about" },
        { path: "/user/:id", component: User,    props: true },
        { path: "/:pathMatch(.*)*", component: NotFound }
    ]
});

19.3 在模板里使用

App.vue
<template>
    <nav>
        <RouterLink to="/">首页</RouterLink>
        <RouterLink :to="{ name: 'about' }">关于</RouterLink>
    </nav>

    <main>
        <RouterView />
    </main>
</template>

<script setup>
    import { useRouter, useRoute } from "vue-router";
    const router = useRouter();

    function go() {
        router.push("/about");
    }
</script>

19.4 路由参数

使用
<RouterLink :to="`/user/${user.id}`">{{ user.name }}</RouterLink>

// User.vue
import { useRoute } from "vue-router";
const route = useRoute();
console.log(route.params.id);   // url 上的 id
console.log(route.query.tab);   // ?tab=xxx

19.5 导航守卫

router
router.beforeEach((to, from) => {
    if (to.requiresAuth && !isLoggedIn())
        return { name: "login", query: { redirect: to.fullPath } };
});

Pinia 状态管理 ⭐⭐⭐

20.1 安装与定义 Store

stores/user.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";

export const useUserStore = defineStore("user", () => {
    const name    = ref("游客");
    const token   = ref("");

    const isLogin = computed(() => !!token.value);

    function login(u) {
        name.value  = u.username;
        token.value = u.token;
    }
    function logout() {
        name.value = "";
        token.value = "";
    }

    return { name, token, isLogin, login, logout };
});

20.2 在组件中使用

任意组件
<script setup>
    import { useUserStore } from "@/stores/user";
    import { storeToRefs } from "pinia";

    const userStore = useUserStore();
    const { name, isLogin } = storeToRefs(userStore);
    const login = userStore.login;   // 方法直接拿

    function doLogin() {
        login({ username: "Alice", token: "abc" });
    }
</script>

<template>
    <p>欢迎 {{ name }} ({{ isLogin ? "在线" : "离线" }})</p>
    <button @click="doLogin">登录</button>
</template>
对比 Vuex:Pinia 写法更简洁(TypeScript 友好),是官方推荐。

过渡与动画 ⭐⭐

Vue SFC
<template>
    <button @click="show = !show">切换</button>
    <Transition name="fade">
        <p v-if="show">你好</p>
    </Transition>
</template>

<style>
    .fade-enter-active, .fade-leave-active { transition: opacity 0.5s; }
    .fade-enter-from,   .fade-leave-to   { opacity: 0; }
</style>

21.1 TransitionGroup 列表过渡

Vue
<TransitionGroup name="list" tag="ul">
    <li v-for="t in todos" :key="t.id">{{ t.text }}</li>
</TransitionGroup>

.list-enter-from { opacity: 0; transform: translateY(-20px); }
.list-leave-to   { opacity: 0; transform: translateX(20px); }
.list-move       { transition: transform 0.5s; }

自定义指令 ⭐

Vue
// 全局
app.directive("focus", {
    mounted(el) { el.focus(); }
});

// 局部
const vDemo = {
    mounted(el, binding) {
        el.style.color = binding.value;
    }
};

<input v-focus>
<p v-demo="color">看我</p>

v-once / v-memo 等性能优化 ⭐

指令作用
v-once只渲染一次(极致静态)
v-memo依数组决定是否更新
v-pre跳过编译,原样显示
Vue
<div v-once>永远不再变</div>
<div v-pre>{{ 不会编译 }}</div>

// v-memo (3.2+)
<div v-memo="[item.id, item.text]">{{ item.text }}</div>

Vite 构建工具 ⭐⭐

24.1 核心特点

24.2 常用命令

Vite
npm run dev       # 开发服务器
npm run build     # 生产构建(dist/)
npm run preview   # 预览构建结果

24.3 vite.config.js

vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { fileURLToPath, URL } from "node:url";

export default defineConfig({
    plugins: [vue()],
    resolve: {
        alias: {
            "@": fileURLToPath(new URL("./src", import.meta.url))
        }
    },
    server: { port: 5173 }
});

插件与混入 ⭐

25.1 插件(plugin)

Vue
// 全局属性
app.config.globalProperties.$http = axios;

// install 风格
export default {
    install(app, options) {
        app.directive("demo", {...});
        app.provide("key", options);
    }
};

25.2 混入 mixin(不推荐)

mixin.js
export const mouseMixin = {
    data() { return { x: 0, y: 0 }; },
    mounted() { window.addEventListener("mousemove", this.upd); }
};

import { mouseMixin } from "./mixin.js";
export default { mixins: [mouseMixin] };

// 推荐改写为 composable(见 16 章 useMouse)

项目一:Todo 应用(Composition API)⭐⭐⭐

项目 1:Todo List(组合式 API)
难度:⭐⭐核心:ref + computed + v-model
useTodo.js
import { ref, computed } from "vue";

export function useTodo() {
    const list  = ref(JSON.parse(localStorage.getItem("todo") || "[]"));
    const text  = ref("");
    const filter = ref("all");

    function save() {
        localStorage.setItem("todo", JSON.stringify(list.value));
    }
    function add() {
        if (!text.value.trim()) return;
        list.value.push({ id: Date.now(), text: text.value, done: false });
        text.value = "";
        save();
    }
    function toggle(id) {
        const it = list.value.find(t => t.id === id);
        if (it) it.done = !it.done;
        save();
    }
    function remove(id) {
        list.value = list.value.filter(t => t.id !== id);
        save();
    }

    const view = computed(() =>
        filter.value === "all"     ? list.value :
        filter.value === "active" ? list.value.filter(t => !t.done) :
                                list.value.filter(t =>  t.done)
    );

    return { list, text, filter, view, add, toggle, remove };
}

项目二:图书管理 SPA ⭐⭐⭐

项目 2:图书管理(Vue Router + Pinia)
难度:⭐⭐⭐⭐综合:路由 + 状态管理 + CRUD

27.1 store

stores/book.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";

export const useBookStore = defineStore("book", () => {
    const list = ref([
        { id: 1, title: "Vue 入门", author: "张三" },
        { id: 2, title: "JavaScript 高级", author: "李四" }
    ]);
    const count = computed(() => list.value.length);

    function add(book) { list.value.push({ id: Date.now(), ...book }); }
    function del(id) { list.value = list.value.filter(b => b.id !== id); }
    function find(id) { return list.value.find(b => b.id === Number(id)); }

    return { list, count, add, del, find };
});

27.2 路由 + 视图

main.js
import { createApp } from "vue";
import { createPinia } from "pinia";
import router from "./router";
import App from "./App.vue";

createApp(App).use(createPinia()).use(router).mount("#app");
BookList.vue
<script setup>
    import { useBookStore } from "@/stores/book";
    import { storeToRefs } from "pinia";
    import { useRouter } from "vue-router";

    const store = useBookStore();
    const { list, count } = storeToRefs(store);
    const router = useRouter();
</script>

<template>
    <h2>图书列表({{ count }})</h2>
    <RouterLink to="/book/new">新增</RouterLink>
    <ul>
        <li v-for="b in list" :key="b.id">
            <RouterLink :to=`/book/${b.id}`">{{ b.title }}</RouterLink>
            <button @click="store.del(b.id)"></button>
        </li>
    </ul>
</template>

项目三:电商商品列表 ⭐⭐⭐

项目 3:商品列表(Fetch + 过滤 + 详情页)
难度:⭐⭐⭐综合:远程数据 + 响应式过滤
Products.vue
<script setup>
    import { ref, computed, onMounted } from "vue";
    import ProductCard from "@/components/ProductCard.vue";

    const products = ref([]);
    const keyword  = ref("");
    const category = ref("all");

    onMounted(async () => {
        const r = await fetch("https://fakestoreapi.com/products");
        products.value = await r.json();
    });

    const list = computed(() => {
        const kw = keyword.value.toLowerCase();
        return products.value.filter(p =>
            (category.value === "all" || p.category === category.value) &&
            p.title.toLowerCase().includes(kw)
        );
    });
</script>

<template>
    <input     v-model="keyword" placeholder="搜索">
    <select   v-model="category">
        <option value="all">全部</option>
        <option value="electronics">电子</option>
        <option value="jewelery">珠宝</option>
    </select>

    <div class="grid">
        <ProductCard
            v-for="p in list" :key="p.id" :product="p" />
    </div>
</template>

<style scoped>
    .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; }
</style>

Vue 速查手册 + 课堂训练

29.1 API 速查

响应式

  • ref(value)
  • reactive(obj)
  • computed(fn)
  • watch(src, cb)
  • watchEffect(fn)

生命周期

  • onMounted
  • onUpdated
  • onUnmounted
  • onBeforeMount
  • onErrorCaptured

组件

  • defineProps()
  • defineEmits()
  • defineExpose()
  • <slot>
  • useSlots/useAttrs

指令

  • v-if / v-show
  • v-for + :key
  • v-model.lazy/number/trim
  • v-on @ + 修饰符
  • v-bind :
  • v-once / v-memo / v-pre

Router

  • createRouter()
  • <RouterLink />
  • <RouterView />
  • useRouter()
  • useRoute()
  • router.beforeEach

Pinia

  • defineStore(id, setup)
  • storeToRefs(store)
  • $state / $patch
  • $subscribe / $onAction

依赖注入

  • provide(key, value)
  • inject(key, default)

工具

  • nextTick()
  • useTemplateRef()
  • app.use(plugin)
  • app.component()
  • app.directive()

29.2 课堂训练(12 题)

Q1. 在 setup 中,ref 与 reactive 的核心区别是?
  1. A. ref 只能装基本类型,reactive 只能装对象
  2. B. ref 用 .value 取值;reactive 直接拿属性
  3. C. 两者完全一样
  4. D. reactive 是 ES6 proxy 实现
答案:B。ref 任何值通用,模板自动解包;reactive 仅对象,直接拿属性。
Q2. v-if 与 v-show 的区别?
  1. A. 完全相同
  2. B. v-show 不支持 boolean
  3. C. v-if 在 false 时不创建元素;v-show 始终在,CSS 切换
  4. D. v-show 性能始终更好
答案:C。v-if 是真渲染/销毁,v-show 是切换 display。
Q3. v-for 列表必须加 :key,为什么?
  1. A. 帮助 Vue 高效复用和重排 DOM
  2. B. 必需否则报错
  3. C. 纯属风格问题
  4. D. 必须用 index
答案:A。key 是 diff 算法的标识,最好用稳定 id,不要用 index。
Q4. 父 → 子传值用?子 → 父传值用?
  1. A. props / ref
  2. B. slot / slot
  3. C. attrs / props
  4. D. props / emit
答案:D。这是 Vue 组件通信的基础模式。
Q5. computed 与 method 的差别?
  1. A. 性能完全相同
  2. B. computed 有缓存,依赖不变不重算
  3. C. method 有缓存,computed 没有
  4. D. computed 不能传参
答案:B。这是 computed 作为"派生数据"的核心优势。
Q6. onMounted 里适合做什么?
  1. A. 修改 props
  2. B. 写响应式数据声明
  3. C. 发请求、操作 DOM、绑定事件
  4. D. 都不合适
答案:C。DOM 已渲染好,可以与外部交互。
Q7. <RouterView> 与 <RouterLink> 的区别?
  1. A. RouterLink 是路由链接,RouterView 渲染当前路由对应的组件
  2. B. RouterView 生成链接
  3. C. 两者一样
  4. D. RouterLink 是 v-if 的另一种写法
答案:A。记住:Link = a,View = outlet。
Q8. Pinia 相较 Vuex 的最大好处?
  1. A. 支持异步
  2. B. 更稳定
  3. C. 体积更小
  4. D. 写法更简洁,TS 友好,无需 mutations
答案:D。官方推荐 Pinia 替代 Vuex。
Q9. ref 在模板中会自动 .value 解包,例外是?
  1. A. v-for 中不会解包
  2. B. 当 ref 是对象/数组的 reactive 属性时不会解包
  3. C. 永远不会解包
  4. D. 在 setup 中不会解包
答案:B。如果 ref 被嵌套为 reactive 对象的属性,将保持 .value 形态。
Q10. 想让多个组件共享一份响应式数据,应该?
  1. A. 每个组件都自己 ref 一次
  2. B. 用 mixin
  3. C. 用 Pinia / provide-inject
  4. D. window 全局变量
答案:C。Pinia 是跨组件/页面共享的最佳答案。
Q11. <script setup> 的最大优势?
  1. A. 提升运行时性能
  2. B. 顶层变量自动暴露,少写 return,TS 更友好
  3. C. 兼容老 IE
  4. D. Vue2 不能用
答案:B。setup 语法糖是 Composition API 的最佳实践。
Q12. Vite 相比 webpack 的关键优势?
  1. A. 基于原生 ESM 启动秒级,开发体验极快
  2. B. 体积更小
  3. C. SEO 更好
  4. D. 服务端渲染更好
答案:A。Vite 已成 Vue 默认工具链。

写在最后:Vue 是现代化前端的入口

🎓 与本系列衔接
HTML     → 网页骨架
CSS      → 网页样式
JavaScript→ 网页交互
Vue      → 组件化 + 响应式前端框架

学完 Vue 之后,下一步可往 Nuxt 3 SSRElement Plus / Naive UIPinia 高级用法TypeScript 强化 继续深入。

推荐资源:Vue 官方文档(vuejs.org)、Vue Mastery、Vue School、Vite 官方文档。