Skip to content

第 13 章 · Vue 入门

一句话开篇:Vue 让你像写 HTML 一样写组件,配上响应式系统,数据一变界面自动跟着变。


0. 生活类比

类比一:Vue = 宜家家具,React = 乐高

  • React 像乐高积木:基础积木齐全,但没有说明书,要装哪种家具自己拼,自由度高但要设计能力。
  • Vue 像宜家家具:每个组件自带"组装说明书"(模板),配套螺丝刀和说明卡(指令),按图组装就能搞定,对新手特别友好。

类比二:响应式系统 = 小蜜蜂跟踪员

Vue 给每个数据都派了一个"小蜜蜂",盯着它什么时候被读、什么时候被改。

  • 谁读了它(哪个组件 / computed) → 记下来"依赖关系"
  • 谁改了它 → 通知所有依赖它的人:"你该重画了!"

你只管改数据,谁该更新交给小蜜蜂。

类比三:单文件组件(SFC)= 三明治

.vue 文件像三明治:

┌──────────────────┐
│  <template>      │  面包:界面长什么样
├──────────────────┤
│  <script setup>  │  夹心:逻辑、数据
├──────────────────┤
│  <style scoped>  │  面包:样式(只作用本组件)
└──────────────────┘

打包成一份,吃起来真香。


1. Vue 是什么

  • 定位:尤雨溪 2014 年开发的渐进式前端框架
  • 当前主流版本:Vue 3(2020 年发布)
  • 核心理念
    1. 响应式:数据变化自动驱动视图更新(基于 ES6 Proxy)
    2. 组件化:单文件组件 .vue = 模板 + 脚本 + 样式
    3. 声明式渲染:用模板语法描述 UI
    4. 渐进式:可以只用核心库写 widget,也可以用全家桶(Vue Router + Pinia)写 SPA

Vue 2 vs Vue 3 关键差异

维度Vue 2Vue 3
响应式Object.definePropertyProxy(更强大)
主推 APIOptions APIComposition 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-ifv-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 对比

维度computedwatch
用途派生数据监听变化做副作用
返回值有(响应式 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 APIComposition API
created(setup 本身就是)
mountedonMounted
updatedonUpdated
unmountedonUnmounted
beforeMountonBeforeMount
beforeUpdateonBeforeUpdate
errorCapturedonErrorCaptured

8. Composition API vs Options API

维度Options APIComposition API
组织方式按选项分类(data/methods/computed)按逻辑功能聚合
复用方式mixin(命名冲突难追踪)自定义组合函数 useXxx
TS 支持一般优秀
适合简单组件、新手友好复杂逻辑、复用性强

Vue 3 Composition vs React Hooks 对比

维度Vue Composition APIReact Hooks
触发方式setup 函数只执行一次每次渲染都重新执行
依赖追踪Proxy 自动追踪手动写依赖数组
闭包陷阱无(响应式自动更新)有(陈旧闭包)
状态更新直接改 .value必须调用 setter
重新渲染触发依赖被修改时setState 时

Vue 心智负担更低(无需关心依赖数组、闭包陷阱),React 更"函数式"(每次渲染都是纯函数调用)。


9. 实战案例

详见 examples/ 目录:

  • basic.html — Options API + 基础指令
  • composition-api.html — setup + ref + computed + watch
  • components.html — 组件 + props + emit + slot

demo/index.html商品搜索过滤小应用(Composition API 综合演练)


10. ⚠️ 易踩的坑

  1. 解构 reactive 丢失响应性

    js
    const state = reactive({ count: 0 });
    const { count } = state;       // ❌ count 不再响应
    const { count } = toRefs(state); // ✅
  2. ref 在 JS 里忘记 .value

    js
    const n = ref(0);
    n++;            // ❌ 没用
    n.value++;      // ✅
    // 模板里自动解包:{{ n }} ✅
  3. v-forv-if 同行 Vue 3 里 v-if 优先级高于 v-for,会拿不到循环变量。要么外包一层 <template v-if>,要么先 computed 过滤。

  4. v-html 引发 XSS 永远不要把用户输入直接 v-html,会注入脚本。

  5. 修改 props props 是单向数据流,不能直接改。要么 emit 通知父级改,要么 defineModel(Vue 3.4+)。

  6. watch 默认 deep: false 监听对象内部属性变化要 { deep: true },否则只在引用变化时触发。

  7. 生命周期顺序 父 beforeCreate → 父 created → 父 beforeMount → 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted → 父 mounted。

  8. CDN 引入版本不对vue.global.js(含编译器)才能在浏览器里写 template;vue.runtime.js 需要预编译。


11. 一句话总结

Vue 让你用 HTML 模板 + 响应式数据写组件,单文件三件套(template/script/style)天然清晰,Composition API 让逻辑按功能聚合且支持复用。


12. 延伸阅读