配套代码见本仓库:App.vue / components/ / composables/
运行:npm install && npm run dev


一、Composition API 与 Options API 有什么区别?

Options API(Vue2 风格)

逻辑按选项类型组织:datamethodscomputedwatch生命周期 各写一块。

1
2
3
4
5
6
export default {
data() { return { count: 0 } },
computed: { double() { return this.count * 2 } },
methods: { increment() { this.count++ } },
mounted() { console.log('mounted') }
}

Composition API(Vue3 风格,<script setup>

逻辑按功能组织:同一个功能的「状态 + 计算 + 方法 + 生命周期」写在一起。

1
2
3
4
5
6
7
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() { count.value++ }
onMounted(() => console.log('mounted'))
</script>

核心区别

维度 Options API Composition API
组织方式 按选项类型(data/methods…) 按功能(相关业务逻辑聚合)
逻辑复用 mixins / 作用域插槽(易冲突、来源模糊) composable 函数(显式返回、来源清晰)
TypeScript this 类型推导困难 天然类型友好,无 this
代码规模 组件越大,「跳读」越痛苦 相关逻辑聚拢,可维护性高
响应式入口 data() 返回对象 ref / reactive 显式声明
this 依赖 this 上下文 几乎不需要 this

一句话:Options API 把代码「切碎」到不同选项里;Composition API 让你把「一个功能」的代码「聚拢」在一起,并能被抽成 composable 复用。


二、Vue3 的设计目标是什么?

  1. 更好的 TypeScript 支持:用 Class API / this 推导类型很痛苦,Vue3 用 Composition API + 类型友好的响应式原语,让 TS 成为一等公民。
  2. 更灵活的逻辑复用:取代 mixins 的缺陷(命名冲突、数据来源不清晰),用组合式函数(composable)做显式、可组合的复用。
  3. 更小的体积 & 更好的 Tree-shaking:基于 ES Module,未使用的特性不进包。
  4. 更好的性能:重写响应式系统(Proxy)、编译期优化,运行时更轻。
  5. 更易维护的大型应用:逻辑聚合、组合优于继承、暴露底层能力(reactivity 模块可单独用)。
  6. 渐进式 & 兼容:Options API 依旧保留,老项目可平滑过渡。

三、Vue3 做了哪些优化?

  • 响应式系统重写Object.definePropertyProxy(见第五节)。
  • 编译期优化
    • 静态节点提升(hoist):不变的节点只创建一次。
    • 补丁标记(patch flag):diff 时只检查带 flag 的动态节点。
    • 事件缓存、Block Tree 减少比对范围。
  • Tree-shaking:见第六节。
  • 更小的运行时:核心运行时精简,按需引入。
  • Fragment / Teleport / Suspense 等新内置能力。
  • 自定义渲染器 API:可脱离 DOM(如小程序、Canvas、Native)。

四、用 Vue3 如何设计一个 Modal 组件?

关键设计点(本项目 src/components/Modal.vue 已实现):

  1. Teleport 到 body<Teleport to="body"> 让弹窗脱离父级 DOM 层级,避免 overflow/transform/z-index 影响。
  2. Transition 做动画<Transition name="modal-fade"> 处理进出场过渡。
  3. v-model 控制显隐:用 modelValue + update:modelValue,父组件 <Modal v-model="show" /> 一行搞定。
  4. 插槽(slot)解耦内容
    • 默认插槽放主体内容;
    • #header 自定义标题区;
    • #footer 自定义底部按钮,并通过插槽 propclose 暴露给父组件。
  5. 副作用收敛
    • 打开时锁定 body 滚动(document.body.style.overflow = 'hidden');
    • 支持点击遮罩关闭(@click.self)、按 Esc 关闭(监听 keydown);
    • watch + onBeforeUnmount 在关闭/卸载时清理监听与滚动锁。
  6. 逻辑复用:开关状态放进 useModal() composable,多个弹窗共享同一套「开/关 + 锁滚动」逻辑。
1
2
3
4
5
6
<Modal v-model="isOpen" title="你好">
<p>主体内容</p>
<template #footer="{ close }">
<button @click="close">取消</button>
</template>
</Modal>

五、Vue3 为什么用 Proxy 替代 Object.defineProperty?

Options API / Vue2 的 defineProperty 痛点

  1. 无法监听属性新增/删除obj.newKey = 1 不会触发响应式,必须 Vue.set()
  2. 数组索引/长度变化监听不到arr[0] = xarr.length = 0 不触发更新。
  3. 需要递归遍历:初始化时递归 Object.defineProperty 每个属性,对象大时初始化慢、且「用不到的属性」也被劫持。
  4. 对 Map/Set/Class 等支持差

Vue3 的 Proxy 优势

  • 代理整个对象:天然监听新增/删除属性、数组索引与长度变化,无需 Vue.set
  • 惰性响应式:只在「被访问(get)」时才递归代理子对象,初始化更快、更省内存。
  • 支持更多数据结构:Map、Set、WeakMap 等。
  • 拦截丰富get/set/deleteProperty/has/ownKeys 等 13 种 trap。

举例对比

1
2
3
4
5
6
7
8
9
// Vue2:新增属性不生效,必须 Vue.set
this.user.age = 18 // ❌ 不会触发更新
this.$set(this.user, 'age', 18) // ✅ 需要特殊 API

// Vue3:Proxy 原生支持
const user = reactive({ name: 'Tom' })
user.age = 18 // ✅ 直接生效,无需额外 API
delete user.name // ✅ 删除也生效
user.list[0] = 'a' // ✅ 数组索引修改也生效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 自己体会 Proxy 的「一次性代理 + 惰性递归」
const raw = { nested: { a: 1 } }
const proxy = new Proxy(raw, {
get(target, key, receiver) {
console.log('读取', key)
const res = Reflect.get(target, key, receiver)
// 只有访问到对象时才继续代理它的子对象(惰性)
return typeof res === 'object' && res !== null ? new Proxy(res, this) : res
},
set(target, key, value, receiver) {
console.log('设置', key, value)
return Reflect.set(target, key, value, receiver)
}
})
proxy.nested.a = 2 // 读取 nested → 设置 a 2(deleteProperty、新增 key 都能拦截)
proxy.age = 18 // 设置 age 18(新增属性被 set 拦截,自动响应式)

六、Vue3 中 Tree-shaking 的特性(举例)

原理

Vue3 全部用 ES Module 编写,每个 API 都是独立命名的导出(named export)。打包工具(Rollup / Vite / webpack)在做 Tree-shaking 时,只会保留「被 import 并实际使用」的代码,未使用的代码会在生产构建中被摇掉(dead code elimination)。

举例:内置指令按需打包

Vue3 的指令(如 v-modelv-show)在编译后对应独立的运行时函数:

1
2
3
// 你只用了 v-model
import { vModelText } from 'vue' // 仅此函数被打包
// 没用到 v-show → 对应的 vShow 函数不会被打进最终 bundle

举例:组合式 API 按需引入

1
2
3
// 只引入了你用到的两个 API
import { ref, computed } from 'vue'
// reactive / watch / onMounted 等若没被 import,就不会进入产物

对比 Vue2(为什么 Vue2 难 Tree-shaking)

Vue2 的 API 挂在单一 Vue 构造函数上,是「运行时成员访问」:

1
2
3
4
// Vue2:Vue.nextTick / Vue.set 是 Vue 对象上的属性
// 打包工具无法确定你运行时是否用到,无法静态分析 → 整包保留
import Vue from 'vue'
Vue.nextTick(() => {})

而 Vue3 的 import { nextTick } from 'vue'静态可分析的命名导入,没用到的 nextTick 就会被摇掉。

直观的体积差异

  • 一个只用了 ref + 模板渲染的最小 Vue3 组件,生产包里只包含「运行时核心 + ref + 渲染相关代码」;
  • Vue2 即便只用了一个 nextTick,也往往会把整个 Vue 运行时打进去。

这也呼应了「设计目标」里的「更小体积、更好 Tree-shaking」。


七、Vue3 性能提升主要通过哪些方面体现?

  1. 响应式层(Proxy):惰性代理、免递归初始化、避免 Vue.set,读写拦截更高效。
  2. 编译层优化
    • 静态提升(hoist static):静态节点/属性只创建一次,不进 render 函数每次执行;
    • 补丁标记(patch flag):动态节点打标记,diff 时跳过静态内容,只比对真正变化的部分;
    • 事件处理函数缓存:避免每次 render 都生成新函数;
    • Block Tree:以动态节点为边界减少 vnode 比对数量。
  3. Tree-shaking:按需打包,运行时代码更少。
  4. 更小的运行时核心:相比 Vue2 整体更轻。
  5. 更优的组件更新粒度:依赖追踪精确到「具体的响应式字段」,组件级更新更精准。

附:本仓库文件地图

http://www.rainxy.com/Vue3Learning/

https://gitee.com/run_yu163/vue3-composition_-api-learing.git

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vue3/
├── index.html
├── package.json
├── vite.config.js
├── README.md
├── docs/
│ └── EXPLAIN.md # 本文件:全部概念讲解
└── src/
├── main.js
├── style.css
├── App.vue # 入口演示:两种 API 对比 + Modal 演示
├── components/
│ ├── CounterOptions.vue # Options API 示例
│ ├── CounterComposition.vue # Composition API 示例
│ └── Modal.vue # 可复用 Modal 组件
└── composables/
├── useCounter.js # 计数逻辑复用
└── useModal.js # 弹窗开关 + 滚动锁复用