主题
第 13 章 · Vue 入门
一句话开篇:Vue 让你像写 HTML 一样写组件,配上响应式系统,数据一变界面自动跟着变。
0. 生活类比
类比一:Vue = 宜家家具,React = 乐高
- React 像乐高积木:基础积木齐全,但没有说明书,要装哪种家具自己拼,自由度高但要设计能力。
- Vue 像宜家家具:每个组件自带"组装说明书"(模板),配套螺丝刀和说明卡(指令),按图组装就能搞定,对新手特别友好。
类比二:响应式系统 = 小蜜蜂跟踪员
Vue 给每个数据都派了一个"小蜜蜂",盯着它什么时候被读、什么时候被改。
- 谁读了它(哪个组件 / computed) → 记下来"依赖关系"
- 谁改了它 → 通知所有依赖它的人:"你该重画了!"
你只管改数据,谁该更新交给小蜜蜂。
类比三:单文件组件(SFC)= 三明治
.vue 文件像三明治:
┌──────────────────┐
│ <template> │ 面包:界面长什么样
├──────────────────┤
│ <script setup> │ 夹心:逻辑、数据
├──────────────────┤
│ <style scoped> │ 面包:样式(只作用本组件)
└──────────────────┘打包成一份,吃起来真香。
1. Vue 是什么
- 定位:尤雨溪 2014 年开发的渐进式前端框架
- 当前主流版本:Vue 3(2020 年发布)
- 核心理念:
- 响应式:数据变化自动驱动视图更新(基于 ES6 Proxy)
- 组件化:单文件组件
.vue= 模板 + 脚本 + 样式 - 声明式渲染:用模板语法描述 UI
- 渐进式:可以只用核心库写 widget,也可以用全家桶(Vue Router + Pinia)写 SPA
Vue 2 vs Vue 3 关键差异
| 维度 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式 | Object.defineProperty | Proxy(更强大) |
| 主推 API | Options API | Composition API(也保留 Options) |
| TS 支持 | 一般 | 原生良好 |
| 包大小 | ~30KB | ~14KB(树摇友好) |
| 多根节点 | ❌ template 只能一个根 | ✅ 支持 |
2. 模板语法
Vue 的模板就是合法的 HTML,再加上 {{ }} 插值和 v- 指令。
html
<div id="app">
<h1>{{ title }}</h1>
<p :class="{ active: isActive }">{{ message }}</p>
<button @click="count++">+1 ({{ count }})</button>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
return {
title: 'Hello Vue',
message: 'Welcome',
isActive: true,
count: ref(0),
};
}
}).mount('#app');
</script>插值(文本/属性)
html
<p>{{ name }}</p> <!-- 文本 -->
<a :href="url">link</a> <!-- 属性绑定(v-bind 简写) -->
<input :value="x" /> <!-- 单向绑定 -->
<input v-model="x" /> <!-- 双向绑定 -->
<p v-html="raw"></p> <!-- 原始 HTML(注意 XSS) -->3. 常用指令
| 指令 | 简写 | 作用 |
|---|---|---|
v-bind:attr="x" | :attr="x" | 属性绑定 |
v-on:event="fn" | @event="fn" | 事件监听 |
v-model="x" | — | 双向绑定(表单) |
v-if="cond" / v-else-if / v-else | — | 条件渲染(销毁/创建) |
v-show="cond" | — | 条件显示(用 display:none) |
v-for="item in list" | — | 列表渲染 |
v-text / v-html | — | 设置文本/HTML |
v-slot:name | #name | 插槽 |
v-pre / v-once / v-cloak | — | 性能/编译指令 |
v-if vs v-show
| v-if | v-show | |
|---|---|---|
| 实现方式 | 销毁/创建 DOM | 切换 display |
| 切换开销 | 高 | 低 |
| 初始渲染 | false 时不渲染 | 始终渲染 |
| 适合场景 | 不常切换 | 频繁切换 |
v-for
html
<ul>
<li v-for="(item, i) in list" :key="item.id">
{{ i + 1 }}. {{ item.name }}
</li>
</ul>
<!-- 遍历对象 -->
<li v-for="(val, key) in obj" :key="key">{{ key }}: {{ val }}</li>
<!-- 遍历数字 -->
<li v-for="n in 5" :key="n">{{ n }}</li>⚠️ 必须加
:key,且v-for不要和v-if写在同一元素(Vue 3 中 v-if 优先级更高,会拿不到循环变量)。
v-model 修饰符
html
<input v-model.lazy="x" /> <!-- change 事件而非 input -->
<input v-model.number="x" /> <!-- 转数字 -->
<input v-model.trim="x" /> <!-- 自动 trim -->4. 计算属性 vs 侦听器
computed(计算属性)
派生状态,有缓存——依赖不变就返回缓存。
js
const fullName = computed(() => firstName.value + ' ' + lastName.value);watch(侦听器)
观察某个数据变化时执行副作用(如发请求、操作 DOM)。
js
watch(query, async (newVal, oldVal) => {
results.value = await search(newVal);
});
watch([a, b], ([na, nb], [oa, ob]) => { ... });
// watchEffect:自动追踪依赖
watchEffect(() => {
console.log(`a=${a.value}, b=${b.value}`);
});computed vs watch 对比
| 维度 | computed | watch |
|---|---|---|
| 用途 | 派生数据 | 监听变化做副作用 |
| 返回值 | 有(响应式 ref) | 无 |
| 缓存 | 有 | 无 |
| 异步 | 不支持 | 支持 |
口诀:有返回值用 computed,要做事用 watch。
5. 组件基础
注册与使用
html
<script>
const Hello = {
props: ['name'],
template: `<p>Hi {{ name }}!</p>`
};
const App = {
components: { Hello },
template: `<Hello name="Vue" />`
};
</script>Props(父 → 子)
js
// 子组件
defineProps({
title: String,
count: { type: Number, default: 0 },
user: { type: Object, required: true },
tags: { type: Array, default: () => [] },
validator: { type: String, validator: v => ['a','b'].includes(v) }
});Emit(子 → 父)
html
<!-- 子组件 -->
<button @click="$emit('inc', 5)">+5</button>
<!-- 父组件 -->
<Child @inc="handleInc" />js
// 子组件 setup 写法
const emit = defineEmits(['inc', 'change']);
emit('inc', 5);插槽(slot)
像"挖个坑",让父组件填内容。
html
<!-- 子组件 Card.vue -->
<div class="card">
<header><slot name="header">默认标题</slot></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
</div>
<!-- 父组件使用 -->
<Card>
<template #header>自定义标题</template>
<p>主内容</p>
<template #footer>自定义页脚</template>
</Card>作用域插槽(子传数据给父用)
html
<!-- 子 List.vue -->
<li v-for="item in list">
<slot :item="item" />
</li>
<!-- 父 -->
<List>
<template #default="{ item }">
<strong>{{ item.name }}</strong>
</template>
</List>6. 响应式原理(Vue 3 Proxy)
Vue 2:Object.defineProperty 的痛点
- 无法监听新增属性:
obj.newKey = 1不响应 - 无法监听删除属性
- 无法监听数组索引修改:
arr[0] = x/arr.length = 0 - 需要递归遍历对象一次性"挂哨兵",初始化慢
→ Vue 2 不得不提供 Vue.set / this.$set 打补丁。
Vue 3:Proxy 的优势
Proxy 拦截整个对象的所有操作(get/set/delete/has 等),无需递归预处理:
js
const proxy = new Proxy(obj, {
get(target, key, receiver) {
track(target, key); // 收集依赖
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key); // 触发更新
return result;
}
});响应式工作流(ASCII 图)
┌────────────┐ ┌─────────────┐
│ 模板/computed│ ── 读取 data ──→ │ Proxy get │
│ /watchEffect │ │ 收集依赖 │
└────────────┘ └──────┬──────┘
│
▼
┌─────────────────┐
│ 依赖映射 dep map │
│ data.x → [组件A, │
│ computed B]│
└─────────────────┘
▲
│
┌────────────┐ ┌─────────────┐
│ data.x = ? │ ── 修改 data ──→ │ Proxy set │
└────────────┘ │ 触发依赖 │
└──────┬──────┘
│
▼
🖼 组件重新渲染ref vs reactive
| API | 适合 | 用法 |
|---|---|---|
ref(v) | 基本类型 / 单个值 | 模板里自动解包,JS 中要 .value |
reactive(obj) | 对象/数组 | 不需 .value,但解构会丢失响应性 |
js
const count = ref(0);
count.value++; // JS 中要 .value
const state = reactive({ a: 1, b: 2 });
state.a++; // 直接读写
const { a } = state; // ❌ a 丢失响应性,要用 toRefs(state)7. Composition API(setup)
Vue 3 推荐写法。把"逻辑"按功能聚合,不再被 Options API 的 data/methods/computed 切割。
vue
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
const count = ref(0);
const double = computed(() => count.value * 2);
watch(count, (n) => console.log('count =>', n));
onMounted(() => {
console.log('挂载完成');
});
function inc() { count.value++; }
</script>
<template>
<button @click="inc">{{ count }} (double: {{ double }})</button>
</template>生命周期钩子(Composition)
| Options API | Composition API |
|---|---|
created | (setup 本身就是) |
mounted | onMounted |
updated | onUpdated |
unmounted | onUnmounted |
beforeMount | onBeforeMount |
beforeUpdate | onBeforeUpdate |
errorCaptured | onErrorCaptured |
8. Composition API vs Options API
| 维度 | Options API | Composition API |
|---|---|---|
| 组织方式 | 按选项分类(data/methods/computed) | 按逻辑功能聚合 |
| 复用方式 | mixin(命名冲突难追踪) | 自定义组合函数 useXxx |
| TS 支持 | 一般 | 优秀 |
| 适合 | 简单组件、新手友好 | 复杂逻辑、复用性强 |
Vue 3 Composition vs React Hooks 对比
| 维度 | Vue Composition API | React Hooks |
|---|---|---|
| 触发方式 | setup 函数只执行一次 | 每次渲染都重新执行 |
| 依赖追踪 | Proxy 自动追踪 | 手动写依赖数组 |
| 闭包陷阱 | 无(响应式自动更新) | 有(陈旧闭包) |
| 状态更新 | 直接改 .value | 必须调用 setter |
| 重新渲染触发 | 依赖被修改时 | setState 时 |
Vue 心智负担更低(无需关心依赖数组、闭包陷阱),React 更"函数式"(每次渲染都是纯函数调用)。
9. 实战案例
详见 examples/ 目录:
basic.html— Options API + 基础指令composition-api.html— setup + ref + computed + watchcomponents.html— 组件 + props + emit + slot
demo/index.html — 商品搜索过滤小应用(Composition API 综合演练)
10. ⚠️ 易踩的坑
解构
reactive丢失响应性jsconst state = reactive({ count: 0 }); const { count } = state; // ❌ count 不再响应 const { count } = toRefs(state); // ✅ref在 JS 里忘记.valuejsconst n = ref(0); n++; // ❌ 没用 n.value++; // ✅ // 模板里自动解包:{{ n }} ✅v-for和v-if同行 Vue 3 里 v-if 优先级高于 v-for,会拿不到循环变量。要么外包一层<template v-if>,要么先 computed 过滤。v-html引发 XSS 永远不要把用户输入直接v-html,会注入脚本。修改 props props 是单向数据流,不能直接改。要么 emit 通知父级改,要么
defineModel(Vue 3.4+)。watch默认deep: false监听对象内部属性变化要{ deep: true },否则只在引用变化时触发。生命周期顺序 父 beforeCreate → 父 created → 父 beforeMount → 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted → 父 mounted。
CDN 引入版本不对
vue.global.js(含编译器)才能在浏览器里写 template;vue.runtime.js需要预编译。
11. 一句话总结
Vue 让你用 HTML 模板 + 响应式数据写组件,单文件三件套(template/script/style)天然清晰,Composition API 让逻辑按功能聚合且支持复用。
12. 延伸阅读
- 官方文档:https://cn.vuejs.org/
- 响应式深入:https://cn.vuejs.org/guide/extras/reactivity-in-depth.html
- 下一章:第 14 章 状态管理(Redux / Zustand / Pinia)