2046 字
10 分钟
Vue 3 定位与核心特性概览

Vue 3 的定位#

Vue 是一套构建用户界面的渐进式框架,可以整站采用,也可以只在一个页面上用一部分。

Vue 的文档把这一特点称为“可以被逐步集成”:已有项目不必推倒重来,需要多少就接入多少。

组件之间的数据流是单向的。父组件通过 props 往下传数据,子组件通过事件把变化通知给父组件。

有人用 MVVM 描述 Vue 的架构,Vue 3 的文档不再这么写。提到 MVVM 与 Knockout 的是 Vue 2 的框架对比页。

两套 API 风格#

Vue 3 同时支持 Options API 和 Composition API,两者都还在支持范围内,可以混着用。

Options API 按选项组织代码:data 放状态,methods 放方法,computed 放计算属性。组件小的时候,这样写读起来更直接。

Composition API 按功能组织代码:一个功能的状态、计算属性、侦听和生命周期钩子写在一起。组件变大以后,逻辑仍然聚在一处。新写的复杂组件推荐用组合式 API。

响应式:Proxy 拦截属性的读写#

Vue 3 的响应式建立在 ES6 Proxy 上。读取属性时收集依赖,写入属性时触发更新。get 和 set 里再用 Reflect 转发到原对象,保持默认行为。

Vue 2 用 Object.defineProperty 逐个属性做劫持,有几类改动监听不到。换成 Proxy 之后,这些改动都能直接触发更新。

场景Vue 2Vue 3
新增属性不触发,要用 Vue.set / $set直接触发
删除属性不触发,要用 Vue.delete直接触发
给数组下标赋值不触发直接触发
修改数组 length不触发直接触发
push、pop、shift、unshift、splice、sort 和 reverse靠改写过的数组方法触发直接触发

Proxy 拦截的是属性的读取和写入,双向绑定不归它管。双向绑定由 v-model 实现,落在一个 prop 加一个对应的 update: 事件上。

嵌套对象用惰性代理:只有访问到某一层,那一层才被包成 Proxy。

编译期的三项优化#

模板会编译成渲染函数。纯运行时的虚拟 DOM 实现要在每次更新时遍历整棵树,Vue 3 把一部分判断挪到了编译期。

静态提升#

静态节点在编译时被提到渲染函数外面,只创建一次。之后的渲染直接复用,不再重新创建。

PatchFlag#

编译时给动态节点打上二进制位标记,运行时只对比被标记的部分,静态节点整块跳过。

标记值含义
TEXT1动态文本
CLASS2动态 class
STYLE4动态 style
PROPS8动态属性,不含 class 和 style

完整列表里还有 FULL_PROPS、NEED_HYDRATION、STABLE_FRAGMENT、KEYED_FRAGMENT、UNKEYED_FRAGMENT、NEED_PATCH、DYNAMIC_SLOTS 和 DEV_ROOT_FRAGMENT。负数标记留给特殊情况:CACHED = -1、BAIL = -2。

Block Tree#

一个 Block 是一段结构稳定的模板片段。创建时它把后代里的动态节点收进一份列表,Diff 只遍历这份列表,范围从整棵树缩到动态节点。

Fragment:多个根节点#

Vue 3 的模板允许多个根节点,不必再套一层 div,渲染结果里也就少一层包装。

MultiRoot.vue
<template>
<div>根节点 1</div>
<div>根节点 2</div>
</template>

多个根节点时,属性不会自动继承到某一个根节点上。父组件传下来的属性要自己绑到 $attrs 上,否则 Vue 会打印一条警告。

render 函数和 JSX 同样可以返回多个根节点。

Tree-shaking:按需引入#

Vue 3 的运行时按模块拆分,API 都走具名导出。打包工具发现某个导出没人用,就不会把它放进产物。

import { ref, computed, watch } from 'vue'

Vue 2 把全局 API 挂在 Vue 对象上,Vue.nextTick() 这类调用没法被 tree-shaking 剔除。迁移到 Vue 3 时改成具名导入,只留用到的函数。

import { nextTick } from 'vue'

组合式 API 的组成#

组合式 API 包含的是一组函数。状态用 ref、reactive、computed,侦听用 watch 和 watchEffect,生命周期用 onMounted、onUnmounted 这类钩子,跨层级传值用 provide 和 inject。

组合式 API 配合 <script setup> 使用,可以省掉 setup() 函数,也不用手动 return 顶层绑定。<script setup> 是单文件组件下的推荐写法。

Suspense:等待异步依赖#

<Suspense> 是内置组件,用来协调组件树里的异步依赖。等待期间先显示 fallback,依赖都解决之后再切到默认内容。

它等待的异步依赖有两类:带异步 setup() 的组件,以及 defineAsyncComponent 定义的异步组件。初次渲染没有遇到异步依赖时,它直接进入已解决状态。

<script setup> 里出现顶层 await,这个组件就编译成异步 setup(),成为 <Suspense> 的异步依赖。

AsyncDashboard.vue
<script setup>
const res = await fetch('/api/posts')
const posts = await res.json()
</script>
<template>
<p>{{ posts.length }}</p>
</template>

父组件里,default 和 fallback 两个插槽各写一个直接子节点:

App.vue
<template>
<Suspense>
<AsyncDashboard />
<template #fallback>
<p>加载中</p>
</template>
</Suspense>
</template>

Teleport:把 DOM 传送出去#

<Teleport> 把模板的一部分渲染到组件 DOM 层级之外的节点里,组件的逻辑层级不变。

弹窗和抽屉经常被父级的 overflow: hidden 裁掉,或者被 z-index 压住。传送到 body 下可以避开这类冲突。

Modal.vue
<template>
<Teleport to="body">
<div class="modal">
弹窗内容渲染在 body 下
</div>
</Teleport>
</template>

to 接收 CSS 选择器字符串,也可以直接传一个 DOM 元素。disabled 用来临时关掉传送,让内容回到原位。

Vue 3.5 起,<Teleport> 多了 defer 属性:当目标节点要由 Vue 在本次渲染中稍后创建时,defer 会把目标的解析推迟到本次挂载完成之后。

同一个目标可以挂多个 Teleport,后挂的内容按顺序追加在后面。数据和事件留在原组件里,不受传送影响。

一个组件上的多个 v-model#

Vue 3 允许在一个组件上写多个 v-model,分别绑定不同的数据,替代了 Vue 2 的 .sync。

UserForm.vue
<!-- 父组件 -->
<UserForm v-model:name="userName" v-model:age="userAge" />

子组件声明这几个 prop,再逐个发对应的 update: 事件:

UserForm.vue
<script setup>
defineProps(['name', 'age'])
const emit = defineEmits(['update:name', 'update:age'])
</script>

defineModel 把这对声明并成一行。它在 3.3 作为实验性功能出现,3.4 转为稳定,API 文档标注的可用版本是 3.4 及以上:

UserForm.vue
<script setup>
const name = defineModel('name')
const age = defineModel('age')
</script>
<template>
<input v-model="name" />
<input v-model="age" />
</template>

事件参数需要类型时,用具名元组写法声明,3.3 及以上可用:

const emit = defineEmits<{
change: [id: number]
update: [name: string]
}>()

TypeScript 支持#

Vue 3 的源码用 TypeScript 重写,自带类型定义。要在项目里用起来,还需要三处配合:

  • 单文件组件上写 lang="ts"
  • 命令行做类型检查装 vue-tsc
  • 编辑器里的类型提示装 Vue - Official 扩展(前身是 Volar)

多数场景的类型能自动推导出来:

Counter.vue
<script setup lang="ts">
const count = ref(0) // Ref<number>
count.value = '123' // 报错:不能把 string 赋给 number
</script>

推导覆盖不了所有情况。值是联合类型、要标注具体的 DOM 元素、或者 props 结构复杂时,泛型还是要自己写:

const year = ref<string | number>('2020')
const el = ref<HTMLInputElement | null>(null)
interface Props {
user: User
visible: boolean
}
const props = defineProps<Props>()

props 结构复杂时,常见做法是用 interface 定义类型,再传给 defineProps<Props>()。

第 1 篇,共 19 篇本文属于系列

Vue 3 定位与核心特性概览
https://www.choria.top/posts/vue3-guides/
作者
乔瑞雅·克林威尔
发布于
2026-04-08
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 214 天

部分内容可能已过时