Vue 核心原理
Vue 是一套渐进式框架:核心只关心视图层,但官方路由(Vue Router)、状态库(Pinia)可以按需加入。Vue 3 用 TypeScript 重写,引入 Composition API,性能与开发体验全面升级。
一、Vue 3 简介与安装
| 方式 | 命令 | 适用 |
|---|---|---|
| 脚手架 | npm create vue@latest | 官方推荐,含路由/Pinia/TS 选项 |
| Vite | npm create vite@latest my-app -- --template vue | 轻量起步 |
| CDN | 引入 vue.global.prod.js | 简单演示 |
html
<!-- CDN 方式快速体验 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const count = ref(0);
return { count };
},
template: `<button @click="count++">{{ count }}</button>`
}).mount("#app");
</script>Vue 3 的核心能力:模板系统 + 响应式系统 + 组件系统,三者共同构成"状态变、视图变"的声明式体验。
二、模板语法
2.1 插值
vue
<template>
<p>{{ message }}</p> <!-- 文本插值,支持任意 JS 表达式 -->
<p v-text="message"></p> <!-- 指令版插值 -->
<p v-html="html"></p> <!-- 渲染 HTML(小心 XSS) -->
</template>2.2 常用指令
| 指令 | 作用 | 示例 |
|---|---|---|
v-bind | 绑定属性,缩写 : | <img :src="url"> |
v-on | 绑定事件,缩写 @ | <button @click="add"> |
v-model | 表单双向绑定 | <input v-model="name"> |
v-if / v-else-if / v-else | 条件渲染(销毁/重建) | <p v-if="ok"> |
v-show | 条件显示(仅切换 display) | <p v-show="ok"> |
v-for | 列表渲染 | <li v-for="i in list" :key="i.id"> |
v-once | 只渲染一次 | <p v-once></p> |
v-memo | 缓存子树(Vue 3.2+) | <div v-memo="[a]"> |
vue
<template>
<div>
<img :src="user.avatar" :alt="user.name" />
<button @click="count++">+1</button>
<input v-model.trim="keyword" placeholder="搜索" />
<ul>
<li v-for="item in filteredList" :key="item.id">{{ item.name }}</li>
</ul>
<p v-if="list.length === 0">暂无数据</p>
</div>
</template>2.3 事件修饰符
vue
<template>
<!-- 阻止冒泡与默认行为 -->
<a @click.stop.prevent="go">跳转</a>
<!-- 键盘事件:按回车触发 -->
<input @keyup.enter="submit" />
<!-- 只触发一次 -->
<button @click.once="once">只响应一次</button>
<!-- 系统修饰键 -->
<div @click.ctrl.exact="ctrlClick">仅 Ctrl+点击 生效</div>
</template>| 修饰符类别 | 常用项 |
|---|---|
| 事件 | .stop .prevent .once .capture .self |
| 按键 | .enter .tab .esc .space .delete |
| 系统键 | .ctrl .shift .alt .meta .exact |
| 鼠标 | .left .right .middle |
三、Options API 与 Composition API
3.1 Options API(选项式)
vue
<script>
export default {
data() { return { count: 0 }; },
computed: { double() { return this.count * 2; } },
watch: { count(newVal) { console.log(newVal); } },
methods: { add() { this.count++; } },
mounted() { console.log("挂载完成"); }
};
</script>3.2 Composition API(组合式)
vue
<script setup>
import { ref, computed, watch, onMounted } from "vue";
const count = ref(0);
const double = computed(() => count.value * 2);
watch(count, (val) => console.log(val));
function add() { count.value++; }
onMounted(() => console.log("挂载完成"));
</script>| 对比 | Options API | Composition API |
|---|---|---|
| 组织方式 | 按选项分块(data/methods/…) | 按逻辑功能聚合 |
| 复用逻辑 | mixin(命名冲突、来源不明) | 自定义 Hook(useXxx) |
| 类型推导 | 依赖 this,较弱 | 天然友好 |
| 学习曲线 | 平缓 | 略陡,需理解 ref/reactive |
| 推荐场景 | 简单组件、老代码 | 新项目、复杂逻辑 |
<script setup> 是 Vue 3.2+ 的组合式 API 语法糖:顶层声明自动暴露给模板,无需 return。
3.3 响应式 API 速查
| API | 作用 | 注意 |
|---|---|---|
ref() | 包装基本类型/对象为响应式 | 模板自动解包,JS 中需 .value |
reactive() | 将对象整体变为响应式 | 只能用于对象,深层代理 |
computed() | 派生值,带缓存 | 依赖不变不重算 |
watch() | 监听数据变化执行副作用 | 可监听 ref/reactive/函数 |
watchEffect() | 自动追踪依赖执行 | 无需指定依赖 |
toRefs() | 解构 reactive 保持响应式 | 解构时使用 |
javascript
import { ref, reactive, computed } from "vue";
const count = ref(0);
const state = reactive({ name: "小明", age: 18 });
const desc = computed(() => `${state.name},${count.value}`);
count.value++; // JS 里操作 ref 必须 .value
const { name, age } = toRefs(state); // 解构后仍是响应式四、响应式系统原理
4.1 Proxy 拦截
Vue 3 用 Proxy 取代 Vue 2 的 Object.defineProperty:
javascript
// 简化版:get 时收集依赖,set 时触发更新
function reactive(target) {
return new Proxy(target, {
get(obj, key, receiver) {
track(obj, key); // 依赖收集
return Reflect.get(obj, key, receiver);
},
set(obj, key, value, receiver) {
const result = Reflect.set(obj, key, value, receiver);
trigger(obj, key); // 触发更新
return result;
},
});
}| 对比 | Vue 2(defineProperty) | Vue 3(Proxy) |
|---|---|---|
| 拦截范围 | 只能拦截已声明的 key | 全对象任意 key |
| 新增/删除属性 | 不响应,需 Vue.set | 天然响应 |
| 数组索引/长度 | 不响应,需重写方法 | 天然响应 |
| 性能 | 递归一次性转换 | 惰性代理,按需深层 |
4.2 依赖收集与触发更新
响应式的两个阶段:
text
读取属性(模板渲染、computed、watch)
→ track:把当前副作用(effect)记录进该属性的依赖集合
写入属性(赋值)
→ trigger:取出依赖集合,逐个重新执行 effect
→ 组件重新渲染一个响应式属性对应一个依赖集合(Set),全局用 WeakMap 按"对象 → key → 依赖"组织。这就是"发布-订阅"模式:数据是发布者,渲染副作用是订阅者。
text
WeakMap<target, Map<key, Set<effect>>>
读属性 → 收集 effect 进集合
写属性 → 取出集合里的 effect 全部执行五、模板编译流程
text
模板(template 字符串 / .vue 文件)
→ 解析器 parse:生成 AST(抽象语法树)
→ 转换器 transform:AST 标记动态节点、静态节点、事件等
→ 代码生成器 generate:产出 render 函数
→ 运行时:render 函数执行 → 虚拟 DOM → 真实 DOMvue
<!-- 模板 -->
<template>
<div class="box">{{ count }}</div>
</template>编译产出的 render 函数(简化):
javascript
import { toDisplayString, openBlock, createElementBlock } from "vue";
export function render(_ctx, _cache) {
return (openBlock(), createElementBlock("div", { class: "box" },
toDisplayString(_ctx.count), 1 /* TEXT 动态节点标记 */));
}编译期标记(patchFlag)让运行时只对比动态部分,静态部分直接复用,这是 Vue 3 比 Vue 2 快的重要原因。
六、diff 算法
组件更新时,新旧两棵虚拟节点树进入 patch(打补丁) 流程:
text
新旧 vnode 对比(patch)
1. 类型不同 → 直接替换整个节点
2. 类型相同(都是 div / 同一组件)→ 复用节点,更新 props
3. 文本节点 → 直接替换文本
4. 子节点是数组 → 走 keyed diff带 key 的列表 diff(双端比较优化):
javascript
// 理想情况:key 相同 → 移动复用,不重建
// 朴素情况:无 key → 按索引对比,删除/插入首项会连带重建后续所有项
const list = [
{ id: 1, name: "a" },
{ id: 2, name: "b" },
];vue
<template>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</template>| 对比项 | 有 key | 无 key |
|---|---|---|
| 复用策略 | 按 key 匹配,尽量移动 | 按索引就地更新 |
| 首项插入 | 只插入一项 | 后续所有项重建 |
| 副作用状态 | 正确保留(输入框内容) | 可能错位 |
| 性能 | 大量重排时更优 | 简单静态列表尚可 |
七、组件通信
| 方式 | 方向 | 适用场景 |
|---|---|---|
| props | 父 → 子 | 传递数据 |
| emit | 子 → 父 | 子组件触发事件 |
v-model | 双向 | 表单类封装 |
| 透传属性 | 父 → 孙 | 组件再包装(inheritAttrs) |
| provide / inject | 跨层级 | 祖先给后代注入 |
| 事件总线 | 任意 | 用 mitt,老代码可用 |
| Pinia | 全局 | 共享状态管理 |
vue
<!-- 父组件 -->
<Child :count="count" @add="count++" />
<!-- 子组件 -->
<script setup>
const props = defineProps({ count: Number }); // 声明 props
const emit = defineEmits(["add"]); // 声明事件
</script>
<template>
<button @click="emit('add')">+1</button>
</template>provide / inject 跨层级注入:
vue
<script setup>
// 祖先:提供数据
provide("theme", { color: "dark" });
// 后代:注入使用(可用 inject 的默认值兜底)
const theme = inject("theme", { color: "light" });
</script>Pinia 是 Vue 3 官方推荐状态库(详见状态管理一文)。
八、Vue 3 与 Vue 2 区别
| 维度 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式 | Object.defineProperty | Proxy,全量拦截 |
| 组合逻辑 | mixin | Composition API |
| TypeScript | 支持差 | 一等公民 |
| 虚拟 DOM | 全量对比 | patchFlag 精准对比 + 静态提升 |
| 多根节点 | 单根模板 | 支持 Fragment |
| 事件 API | $on / $off(全局事件) | 移除,改 mitt |
| 全局 API | Vue.use / Vue.prototype | app.use / app.config.globalProperties |
| 性能 | 一般 | 更快、内存更省 |
| 周边生态 | Vuex 3、Vue Router 3 | Pinia、Vue Router 4 |
javascript
// Vue 2 的全局写法
Vue.use(Plugin);
Vue.prototype.$http = http;
// Vue 3 改为应用实例(app)作用域
const app = createApp(App);
app.use(Plugin);
app.config.globalProperties.$http = http;九、要点速查
| 概念 | 一句话记忆 |
|---|---|
| 模板语法 | 插值 + 指令系统,声明式描述视图 |
| Composition API | 按逻辑组织代码,自定义 Hook 复用 |
| 响应式 | Proxy 拦截读写,收集依赖、触发更新 |
| 模板编译 | template → AST → render 函数 |
| diff | 新旧 vnode 对比,key 保证列表复用正确 |
| 组件通信 | props 向下、emit 向上、provide/inject 跨层 |
| Vue 3 vs 2 | Proxy 响应式、组合式 API、patchFlag 提速 |