
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 2 | Vue 3 |
|---|---|---|
| 新增属性 | 不触发,要用 Vue.set / $set | 直接触发 |
| 删除属性 | 不触发,要用 Vue.delete | 直接触发 |
| 给数组下标赋值 | 不触发 | 直接触发 |
修改数组 length | 不触发 | 直接触发 |
push、pop、shift、unshift、splice、sort 和 reverse | 靠改写过的数组方法触发 | 直接触发 |
Proxy 拦截的是属性的读取和写入,双向绑定不归它管。双向绑定由 v-model 实现,落在一个 prop 加一个对应的 update: 事件上。
嵌套对象用惰性代理:只有访问到某一层,那一层才被包成 Proxy。
编译期的三项优化
模板会编译成渲染函数。纯运行时的虚拟 DOM 实现要在每次更新时遍历整棵树,Vue 3 把一部分判断挪到了编译期。
静态提升
静态节点在编译时被提到渲染函数外面,只创建一次。之后的渲染直接复用,不再重新创建。
PatchFlag
编译时给动态节点打上二进制位标记,运行时只对比被标记的部分,静态节点整块跳过。
| 标记 | 值 | 含义 |
|---|---|---|
TEXT | 1 | 动态文本 |
CLASS | 2 | 动态 class |
STYLE | 4 | 动态 style |
PROPS | 8 | 动态属性,不含 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,渲染结果里也就少一层包装。
<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> 的异步依赖。
<script setup>const res = await fetch('/api/posts')const posts = await res.json()</script>
<template> <p>{{ posts.length }}</p></template>父组件里,default 和 fallback 两个插槽各写一个直接子节点:
<template> <Suspense> <AsyncDashboard />
<template #fallback> <p>加载中</p> </template> </Suspense></template>Teleport:把 DOM 传送出去
<Teleport> 把模板的一部分渲染到组件 DOM 层级之外的节点里,组件的逻辑层级不变。
弹窗和抽屉经常被父级的 overflow: hidden 裁掉,或者被 z-index 压住。传送到 body 下可以避开这类冲突。
<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 v-model:name="userName" v-model:age="userAge" />子组件声明这几个 prop,再逐个发对应的 update: 事件:
<script setup>defineProps(['name', 'age'])const emit = defineEmits(['update:name', 'update:age'])</script>defineModel 把这对声明并成一行。它在 3.3 作为实验性功能出现,3.4 转为稳定,API 文档标注的可用版本是 3.4 及以上:
<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)
多数场景的类型能自动推导出来:
<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>()。
Vue 3 学习笔记
连载中分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 214 天
部分内容可能已过时