Vue 2 和 Vue 3 的区别
1. 性能提升 (Performance Improvements)
Vue 3 在性能方面做了大量的优化,主要体现在:
-
更快的渲染速度 (Faster Rendering):
- 静态树提升 (Static Tree Hoisting): Vue 3 的编译器会分析模板,将静态内容(不会改变的元素或属性)提升到渲染函数之外,这样在后续的重新渲染中就可以跳过对这些静态节点的 diff 和 patch。
- 区块优化 (Block Optimization) / 更新类型标记 (Patch Flags): 编译器会基于模板中的动态绑定,为动态节点打上"补丁标记 (Patch Flag)"。在 diff 阶段,Vue 3 只会比较带有这些标记的动态部分,大大减少了需要比较的节点数量。例如,如果一个元素只有文本内容是动态的,那么只会比较文本内容,而不会比较它的 class 或其他属性。
- 更高效的组件初始化: 组件实例创建更快。
-
更小的包体积 (Smaller Bundle Size):
- 更好的 Tree-shaking : Vue 3 的许多全局 API 和内部帮助函数都设计成了可以通过 ES Module import/export 进行 tree-shaking。如果你没有用到某个特性 (比如
v-model
的某个修饰符或<transition>
组件),它就不会被打包进最终的产物。 - 核心库体积显著减小。
- 更好的 Tree-shaking : Vue 3 的许多全局 API 和内部帮助函数都设计成了可以通过 ES Module import/export 进行 tree-shaking。如果你没有用到某个特性 (比如
-
更优的更新算法:
- 除了上述的 Patch Flags,Vue 3 的 diff 算法在某些场景下也进行了优化。
2. 组合式 API (Composition API)
这是 Vue 3 最显著的新特性之一:
- 目的: 解决了 Vue 2 Options API 在大型复杂组件中逻辑分散、难以复用和类型推断不佳的问题。
- 组织方式 : 允许开发者根据逻辑功能(而不是选项类型如
data
,methods
,computed
)来组织代码。 - 核心 :
setup
函数 (或<script setup>
) 作为入口,配合ref
,reactive
,computed
,watch
,watchEffect
以及生命周期钩子 (如onMounted
) 等函数式 API。 - 优势 :
- 更好的逻辑复用: 可以轻松将相关的逻辑抽取成可复用的组合式函数 (Composables)。
- 更灵活的代码组织: 不再受限于 Options API 的固定结构。
- 更好的类型推断: 对于 TypeScript 用户来说,Composition API 提供了远胜于 Options API 的类型推断能力。
- 代码更易于阅读和维护 (当逻辑复杂时)。
- 与 Options API 的关系: Vue 3 完全兼容 Options API,开发者可以根据偏好或项目需求选择使用。
3. 响应式系统 (Reactivity System)
- Vue 2 : 基于
Object.defineProperty
的 getter/setter 劫持。- 缺点 :
- 无法直接检测到对象属性的添加或删除 (需要
Vue.set
和Vue.delete
)。 - 无法直接检测到数组通过索引修改或
length
属性的修改 (需要重写数组方法)。 - 初始化时需要遍历对象的所有属性。
- 无法直接检测到对象属性的添加或删除 (需要
- 缺点 :
- Vue 3 : 基于 ES6
Proxy
。- 优点 :
- 直接代理整个对象,而非单个属性。
- 可以监听到对象属性的新增、删除,以及数组索引和
length
的修改,无需额外 API。 - 初始化性能更好,因为
Proxy
是惰性的,只在访问属性时才进行操作。 - 提供了更细粒度的响应式控制 (如
readonly
,shallowReactive
,shallowRef
)。
- 优点 :
4. 构建工具 (Build Tool)
- Vue 2: 主要依赖 Webpack (通过 Vue CLI)。
- Vue 3 : 官方推荐并默认集成 Vite 。
- Vite 优势 :
- 极速的冷启动: 开发服务器启动速度非常快,无需等待打包。
- 闪电般的热模块替换 (HMR): 更新几乎是即时的。
- 基于原生 ES Module,按需编译。
- Vite 优势 :
5. TypeScript 支持 (TypeScript Support)
- Vue 2: 对 TypeScript 的支持是通过一些额外的库和配置实现的,体验上并非最佳,尤其是在 Options API 中类型推断比较复杂。
- Vue 3 : 从头开始就考虑了 TypeScript,核心代码库本身就是用 TypeScript 编写的。
- Composition API 提供了非常好的类型推断。
- 提供了如
defineComponent
,defineProps
,defineEmits
等辅助函数,增强了 TS 开发体验。
6. 新增核心特性 (New Core Features)
-
Teleport (
<teleport to="body">
):- 允许将组件的一部分模板渲染到 DOM 树中的任意位置,即使这个位置不在当前组件的挂载点下。常用于实现 Modals、Dropdowns、Notifications 等。
-
Fragments:
- 组件模板不再需要单一的根节点。可以有多个平级的根元素。
-
Suspense (
<suspense>
):- 用于协调异步依赖 (如异步组件、带
async setup
的组件)。可以在等待异步内容加载时显示一个 fallback (如 loading 指示器)。
- 用于协调异步依赖 (如异步组件、带
-
自定义渲染器 API (
createRenderer
):- 允许开发者将 Vue 的核心响应式和组件化能力扩展到非 Web 环境,例如原生移动应用 (Weex, NativeScript)、桌面应用 (Electron) 或 WebGL 场景。
-
Emits 组件选项 (
emits: ['myEvent']
):- 类似于
props
选项,用于显式声明组件会触发哪些自定义事件。有助于代码组织、文档化和类型检查。
- 类似于
-
多
v-model
指令:- 一个组件可以支持多个
v-model
绑定,通过参数指定不同的v-model
。例如:v-model:title="pageTitle"
和v-model:content="pageContent"
。
- 一个组件可以支持多个
7. API 变更与移除 (API Changes & Removals)
- 全局 API :
- Vue 2 的全局 API (如
Vue.component
,Vue.directive
,Vue.mixin
,Vue.use
) 现在通过应用实例 (app = createApp(...)
) 来调用 (app.component
,app.directive
等)。这有助于隔离不同应用的配置,也更利于 tree-shaking。
- Vue 2 的全局 API (如
$on
,$off
,$once
实例方法被移除: 用于事件总线 (Event Bus) 的模式不再推荐。推荐使用更显式的组件间通信方式或外部状态管理库。- Filters 被移除: 推荐使用计算属性或方法替代。
$children
实例属性被移除: 不再推荐直接操作子组件实例。应通过 props 和 events 通信。keyCode
支持作为v-on
的修饰符被移除 : 推荐使用具名的按键别名 (如@keyup.enter
) 或检查事件对象的event.key
。
8. 生态系统 (Ecosystem)
- 路由库:Vue Router 4.x (for Vue 3)
- 状态管理:Pinia (Vue 3 官方推荐), Vuex 4.x (for Vue 3)
- UI 库:许多流行的 UI 库都已适配或专门为 Vue 3 开发了新版本 (如 Element Plus, Ant Design Vue, Naive UI)。
Vue 渲染器 (Renderer)
一、渲染器的核心职责与目标
- 核心职责 :
- 初始渲染 (Mounting): 将虚拟 DOM (VNode) 树首次渲染为目标平台的真实视图 (如浏览器的 DOM 树)。
- 更新渲染 (Patching): 当应用状态变化时,高效地计算出新旧 VNode 树之间的差异,并仅将这些差异应用到真实视图上,最小化操作成本。
- 核心目标 :
- 声明式: 开发者只需关心"什么"被渲染 (数据和模板),而无需关心"如何"渲染 (具体的 DOM 操作)。
- 高效性: 通过虚拟 DOM 和优化的 Diff 算法,实现高性能的视图更新。
- 跨平台: 通过抽象的渲染接口,使 Vue 不仅能渲染到浏览器 DOM,还能渲染到其他平台 (如 Native、Canvas)。
二、虚拟 DOM (VNode) - 渲染的基石
- 定义: VNode (Virtual Node) 是一个轻量级的 JavaScript 对象,它是对真实 DOM 节点 (或其他平台视图元素) 的抽象描述。它不是真实的 UI 元素,而是一个"蓝图"。
- 为何需要 VNode? :
- 性能: 直接操作真实 DOM 非常昂贵。VNode 允许 Vue 在内存中进行计算和比较,找出最小变更集,然后批量更新真实 DOM。
- 跨平台: VNode 提供了一个与平台无关的中间层。
- 更强的编程能力: 可以用 JavaScript 完全控制 VNode 的创建和组合,实现更复杂的逻辑。
- VNode 的主要类型与属性 (简化) :
- 类型 (
type
) :- 元素节点 : 字符串 (如
'div'
,'p'
)。 - 组件节点: 组件的选项对象或构造函数。
- 文本节点 : 特殊符号或
Text
标识。 - Fragment (片段) : 允许多个根节点,
Fragment
标识。 - Comment (注释) :
Comment
标识。
- 元素节点 : 字符串 (如
- 属性 (
props
) : 包含 HTML attributes, DOM properties, 事件监听器,class
,style
, 组件的 props 等。 - 子节点 (
children
): 数组 (包含其他 VNode) 或字符串 (文本内容)。 - Key (
key
): 在列表渲染中用于 Diff 算法识别和复用节点。 - 元素引用 (
el
): 挂载后,指向对应的真实 DOM 元素。 - 标记位 (Flags - Vue 3 内部优化) :
shapeFlag
: 描述 VNode 自身及其children
的类型 (如元素、组件、文本子节点、数组子节点等),用于快速路径判断。patchFlag
: 由编译器生成,标记动态绑定的类型 (如动态文本、动态 class、动态 props 等),用于靶向更新,大幅减少 Diff 开销。
- 类型 (
三、渲染流程:从 VNode 到真实视图
渲染过程主要分为两个阶段:挂载 (Mount) 和 更新 (Patch) 。核心函数通常是 render
,它内部会调用 patch
。
-
阶段一:挂载 (Initial Mount)
- 入口 : 通常由
createApp(RootComponent).mount('#app')
触发。 - 编译 (Compile-time, if using templates) :
- 将模板字符串编译成一个渲染函数 (Render Function)。
- 编译器会进行优化:静态节点提升、动态内容标记 (Patch Flags) 等。
- 执行渲染函数 : 调用组件的渲染函数 (或
setup
中返回的渲染函数),生成根组件的 VNode 树。 patch(null, n2, container, anchor)
:n1
(旧 VNode) 为null
,表示是挂载阶段。n2
(新 VNode) 是刚生成的 VNode 树。container
是挂载的目标 DOM 元素。anchor
是插入时的参考锚点 (可选)。
- 处理 VNode (
processElement
,processComponent
,processText
等) :- 对于元素 VNode :
- 创建真实 DOM 元素 (
hostCreateElement
)。 - 处理
props
(应用 attributes, class, style, 注册事件监听器 -hostPatchProp
)。 - 递归挂载
children
VNode。 - 将创建的 DOM 元素插入到父容器 (
hostInsert
)。 - 将真实 DOM 元素引用存到
vnode.el
。
- 创建真实 DOM 元素 (
- 对于组件 VNode :
- 创建组件实例。
- 初始化组件 (执行
setup
,处理props
,slots
,建立响应式数据)。 - 设置组件的渲染副作用 (
setupRenderEffect
):这个effect
会执行组件的render
函数得到其子树 VNode,并调用patch
来挂载或更新该子树。 - 首次执行渲染副作用,挂载组件的子树。
- 对于文本/注释/Fragment VNode: 创建对应的真实 DOM 节点并插入。
- 对于元素 VNode :
- 入口 : 通常由
-
阶段二:更新 (Patching / Diffing)
- 触发: 当组件依赖的响应式数据发生变化时。
- 调度: 更新通常是异步的,Vue 会将待更新的组件的渲染副作用放入微任务队列。
- 重新渲染 : 执行组件的渲染副作用,生成新的 VNode 树 (
n2
)。 patch(n1, n2, container, anchor)
:n1
是上次渲染生成的旧 VNode 树 (从组件实例或vnode.el._vnode
获取)。n2
是新生成的 VNode 树。
- 比较与更新 :
- 节点类型判断 :
- 如果
n1
和n2
的type
和key
不同,则直接卸载n1
(删除旧 DOM),然后挂载n2
(创建新 DOM)。无需进一步比较。 - 如果类型相同,则进入更细致的
patch
流程。
- 如果
patchElement(n1, n2, container, anchor)
(对于元素 VNode) :- 复用
n1.el
作为n2.el
。 patchProps(el, oldProps, newProps)
: 对比新旧props
,更新变化的 attributes, class, style, 事件监听器等。patchChildren(n1, n2, el, anchor)
: 核心 Diff 算法发生在这里,比较新旧子节点列表。
- 复用
patchComponent(n1, n2, container, anchor)
(对于组件 VNode) :- 判断是否需要更新组件实例 (
shouldUpdateComponent
):通常如果props
变化或slots
变化,组件需要更新。 - 如果需要更新,则更新组件实例的
props
,slots
等,然后重新执行其渲染副作用,得到新的子树 VNode,再递归patch
其子树。 - 如果不需要更新,只需更新
n2.el = n1.el
等引用。
- 判断是否需要更新组件实例 (
- 节点类型判断 :
四、核心 Diff 算法 (patchChildren
)
这是 Vue 渲染性能的关键。目标是以最小的 DOM 操作(创建、删除、移动)将旧子节点列表转换为新子节点列表。
-
简单情况 (Edge Cases First):
- 旧子节点是文本,新子节点是文本: 直接更新文本内容。
- 旧子节点是文本,新子节点是数组: 清空文本,挂载数组中的新 VNode。
- 旧子节点是数组,新子节点是文本: 卸载数组中的旧 VNode,设置文本内容。
- 旧子节点是空,新子节点是数组: 挂载数组中的新 VNode。
- 旧子节点是数组,新子节点是空: 卸载数组中的旧 VNode。
-
数组与数组的比较 (Keyed Diff):
- 双端比较 (Two-ended Comparison - Vue 3 优化) :
- 同步头部 (Sync from start) : 从左到右比较新旧子节点,如果
key
和type
相同,则递归patch
这对节点,并向右移动双端指针。 - 同步尾部 (Sync from end) : 从右到左比较新旧子节点,如果
key
和type
相同,则递归patch
这对节点,并向左移动双端指针。
- 同步头部 (Sync from start) : 从左到右比较新旧子节点,如果
- 处理剩余节点 (Common sequence exhausted) :
- 只有新节点剩余: 批量创建并插入这些新的剩余节点。
- 只有旧节点剩余: 批量卸载这些旧的剩余节点。
- 处理中间乱序部分 (Unknown sequence) :
- 建立旧子节点
key
到index
的映射 (keyToOldIndexMap
): 方便快速查找。 - 遍历剩余的新子节点 :
- 尝试通过
key
在keyToOldIndexMap
中查找对应的旧节点。 - 找到匹配 : 递归
patch
该旧节点和当前新节点。然后需要确定这个旧节点在 DOM 中的正确位置,并可能需要移动它。 - 未找到匹配 (新节点): 创建新的 DOM 元素并插入到当前位置。
- 尝试通过
- 最长递增子序列 (LIS) 优化 (Vue 3) :
- 为了最小化 DOM 移动操作,Vue 会针对剩余的、需要在旧节点中寻找对应项的新节点,构建一个它们在旧序列中索引的数组 (如果找到的话)。
- 计算这个索引数组的最长递增子序列。
- 这个子序列中的节点意味着它们在新旧列表中相对顺序保持不变,因此这些节点对应的 DOM 不需要移动。
- 只需要移动那些不在最长递增子序列中的节点。
- 删除未被复用的旧节点 : 在处理完所有新节点后,那些在旧子节点列表中存在但在新列表中没有对应
key
(或未被复用) 的节点需要被卸载。
- 建立旧子节点
- 无 Key 列表的 Diff (Fallback) : 如果列表没有提供
key
,Vue 会尝试就地复用节点,但这可能导致性能问题和组件状态混乱,通常不推荐。
- 双端比较 (Two-ended Comparison - Vue 3 优化) :
五、编译时优化 (Compile-Time Optimizations - Vue 3)
编译器在将模板转换为渲染函数时,会进行大量优化,辅助运行时 Diff:
- 静态节点提升 (Static Tree Hoisting) :
- 将模板中完全静态的内容(不包含任何动态绑定)提升到渲染函数之外创建,后续渲染时直接复用,无需重新创建 VNode 或进行 Diff。
- 补丁标记 (Patch Flags) :
- 编译器分析动态绑定,为动态 VNode 打上不同的 Patch Flag (位图)。
- 例如,
PatchFlags.TEXT
表示只有文本内容是动态的,PatchFlags.CLASS
表示只有 class 是动态的。 - 在 Diff 时,渲染器可以直接根据这些 Flag 进行靶向更新,跳过不必要的比较。例如,如果一个元素只有
TEXT
标记,那么只会比较其文本内容,而不会比较其props
或style
。
- 区块树 (Block Tree) / 动态子节点收集 :
- 将模板分割成"区块 (Block)"。一个区块是内部具有稳定结构的节点片段。
- 编译器会收集每个区块内所有动态的后代节点。
- 在更新时,只需要遍历这些动态节点进行更新,而不需要遍历整个树。
六、渲染器的可定制性 (createRenderer
)
Vue 3 暴露了 createRenderer
API,允许开发者创建自定义渲染器以适配不同平台。
- Host Config :
createRenderer
接收一个包含平台特定操作的对象,例如:createElement(type)
patchProp(el, key, prevValue, nextValue)
insert(el, parent, anchor)
remove(el)
createText(text)
等。
@vue/runtime-dom
(用于浏览器) 就是通过createRenderer
并传入 DOM 操作的 Host Config 实现的。
Vue 3 响应式系统
1. 响应式基石:Proxy
- 核心机制 :Vue 3 使用
Proxy
对象来劫持对数据的访问(读取get
和设置set
)。当创建响应式对象时(例如通过reactive()
),返回的是一个Proxy
实例。 get
陷阱 (依赖收集track
) :当访问响应式对象的属性时,Proxy
的get
处理器会被触发。此时,Vue 会收集当前正在执行的副作用函数 (effect),并将这个 effect 与被访问的属性关联起来。这个过程称为"依赖收集"。set
陷阱 (触发更新trigger
) :当修改响应式对象的属性时,Proxy
的set
处理器会被触发。此时,Vue 会查找所有依赖 于该属性的副作用函数,并执行它们,从而导致视图更新或其他相关逻辑的执行。这个过程称为"触发更新"。- 优势对比
Object.defineProperty
(Vue 2) :- 全面劫持 :
Proxy
直接代理整个对象,而非单个属性。因此,它可以监听到对象属性的新增、删除,以及数组索引的访问和length
属性的修改,这些在 Vue 2 中需要额外API(如Vue.set
,Vue.delete
)或特殊处理。 - 性能更优 :
Proxy
的劫持发生在对象层面,初始化时不需要遍历所有属性。依赖收集和触发更新的逻辑也更为高效。
- 全面劫持 :
2. 连接数据与更新:副作用函数 (effect
)
- 定义 :副作用函数是指那些依赖于响应式数据,并在这些数据变化时需要重新执行的函数。最典型的副作用函数就是组件的渲染函数(更新DOM),但也可以是用户自定义的
watch
回调或computed
的计算函数。 - 工作流程 :
- 当一个副作用函数执行时,如果它内部访问了响应式对象的属性,这些属性的
get
陷阱会被触发,从而将当前副作用函数注册为这些属性的依赖。 - 当这些响应式属性的值发生变化时(触发
set
陷阱),其关联的副作用函数会被重新执行。
- 当一个副作用函数执行时,如果它内部访问了响应式对象的属性,这些属性的
- 依赖存储 :Vue 内部通常使用
WeakMap
(targetMap) ->Map
(depsMap) ->Set
(dep/effects) 的结构来存储这种依赖关系:targetMap
:键是原始对象 (target),值是depsMap
。depsMap
:键是属性名 (key),值是一个Set
集合,存储所有依赖该属性的副作用函数 (effects)。
3. 异步更新与 nextTick
- 背景:当响应式数据发生变化时,Vue 并不会立即同步执行所有相关的副作用函数来更新DOM。如果同步更新,连续多次数据修改会导致多次不必要的DOM重绘,影响性能。
- DOM 更新策略 :Vue 会将因数据变化而触发的DOM更新任务推入一个异步更新队列中。它会等待当前同步代码执行完毕后,在下一个"tick"(通常是微任务 Microtask)中,批量执行队列中的所有更新。
nextTick
的作用 :- 提供了一个在下次 DOM 更新循环结束之后执行延迟回调的方法。
- 当你修改了数据,并希望在DOM更新完成后执行某些操作(例如获取更新后的DOM元素尺寸),就可以使用
nextTick
。
- 实现原理 :
nextTick
内部主要利用了浏览器的微任务机制(如Promise.resolve().then()
)来确保回调在DOM更新之后、浏览器下次重绘之前执行。
4. 精细化监听:watch
与 watchEffect
-
watch
:- 懒执行 (lazy) :默认情况下,
watch
的回调函数只在被侦听的源数据发生变化后才执行。 - 明确指定侦听源:你需要显式指定要侦听的响应式数据源(可以是 ref、reactive 对象、getter 函数或这些类型的数组)。
- 访问新旧值:回调函数可以接收到变化前后的值 (oldValue, newValue)。
- 深度侦听 (
deep
) 与立即执行 (immediate
):提供选项以支持深度侦听对象内部变化和在初始创建侦听器时立即执行一次回调。 - 实现原理概要 :
- 内部创建一个
effect
,这个effect
的调度器 (scheduler) 负责在数据变化时调用用户提供的回调。 - 在
effect
内部执行用户提供的源 getter 函数(如果是侦听响应式对象,会递归访问其属性以收集依赖,尤其是在deep: true
时)。 - 当依赖变化时,调度器执行回调,并传入新旧值。
- 内部创建一个
- 懒执行 (lazy) :默认情况下,
-
watchEffect
:- 立即执行 :
watchEffect
会立即执行一次其回调函数,并在执行过程中自动追踪其依赖的响应式数据。 - 自动依赖追踪:不需要显式指定侦听源。它会自动收集在回调函数中被访问到的所有响应式依赖。
- 无法访问旧值:回调函数只关心当前值,不提供旧值。
- 适用场景 :当你只需要在某些数据变化时自动执行一段副作用,并且不需要区分新旧值时,
watchEffect
更简洁。 - 实现原理概要 :
- 立即执行用户传入的函数,这个函数本身就是一个副作用
effect
。 - 在执行过程中,函数内访问的响应式数据会收集这个
effect
作为依赖。 - 当任何依赖变化时,这个
effect
会被重新调度执行。
- 立即执行用户传入的函数,这个函数本身就是一个副作用
- 立即执行 :
5. 缓存计算:computed
- 与普通函数的区别 :
- 缓存 :
computed
属性的值是基于其响应式依赖进行缓存的。只要依赖没有发生改变,多次访问computed
属性会立即返回之前的计算结果,而不会重新执行计算函数。普通函数每次调用都会执行。 - 响应式依赖 :
computed
的计算函数内部如果访问了响应式数据,它会自动建立对这些数据的依赖。当依赖变化时,computed
会重新计算并更新其缓存值。
- 缓存 :
- 实现原理概要 :
computed
返回一个特殊的 ref 对象。- 内部创建一个具有 getter 的
effect
。这个 getter 就是用户提供的计算函数。 computed
维护一个_dirty
标记。初始为true
。- 当访问
computed
的.value
时:- 如果
_dirty
为true
,则执行effect
(即用户的计算函数),将其结果存入_value
,并将_dirty
设为false
。在执行计算函数时,会收集其内部的响应式依赖。 - 如果
_dirty
为false
,直接返回缓存的_value
。
- 如果
- 当
computed
所依赖的响应式数据发生变化时,会触发一个调度逻辑,将_dirty
标记重新设为true
,但不会立即重新计算 ,而是等到下次访问.value
时才计算(懒计算特性)。
6. 跨层级数据共享:依赖注入 (provide
/ inject
)
- 目的:允许一个祖先组件向其所有后代组件注入依赖,无论组件层次有多深,都无需一层层手动传递 props。
provide
:在祖先组件中,使用provide
函数来提供数据。可以提供普通值,也可以提供响应式数据(如ref
或reactive
对象)。inject
:在后代组件中,使用inject
函数来注入祖先组件提供的数据。可以指定一个默认值,以防数据未被提供。- 与响应式系统的关联 :
- 如果
provide
的是一个响应式对象 (如ref
或reactive
返回的值),那么当这个响应式数据在祖先组件中发生变化时,所有inject
了这个数据的后代组件中对应的数据也会自动更新,并且会触发这些后代组件的重新渲染(如果它们在模板中使用了这些数据)。 - 这是因为
inject
实际上获取的是对原始响应式数据的引用。当这个数据变化时,所有依赖它的地方(包括后代组件)都会收到通知。
- 如果
7. 创建响应式数据:核心 API
-
reactive(object)
:- 接收一个普通对象,返回该对象的响应式代理。
- 是深层响应式的,即对象内部的嵌套对象也会被
Proxy
包裹。 - 返回的是一个
Proxy
,它与原始对象不等价 (proxy !== originalObject
)。 - 通常用于处理非原始值(对象、数组)。
-
ref(value)
:- 接收一个内部值(可以是任何类型,包括原始值),返回一个响应式的、可变的 ref 对象。
- ref 对象有一个
.value
属性,用于访问或修改内部值。 - 当
.value
被修改时,会触发依赖更新。 - 如果传递给
ref
的是一个对象,它内部实际上会通过reactive()
来实现对该对象的响应式处理。 - 模板中访问 ref 时会自动解包 (unwrap),无需
.value
(但 Vue 3.3+ 在<script setup>
中开启响应性语法糖后,模板中访问响应式变量也不需要.value
)。在 JavaScript/TypeScript 代码中始终需要.value
。
-
readonly(objectOrRef)
:- 接收一个对象(响应式或普通对象)或 ref,返回一个只读的代理。
- 代理是深层只读的。任何对只读代理的修改尝试都会在开发模式下发出警告,并在严格模式下抛出错误。
- 用于保护数据不被意外修改。
-
浅层响应式 (Shallow Reactivity):
shallowReactive(object)
: 创建一个响应式代理,但只对其顶层属性的访问是响应式的。嵌套对象内部的变化不会被追踪。shallowRef(value)
: 创建一个 ref,但只对.value
属性的重新赋值是响应式的。如果.value
是一个对象,对象内部属性的变化不会触发更新,除非整个.value
被替换。- 应用场景:对于大型且层级很深的对象,如果只有顶层属性需要响应式,或者需要手动控制深层对象的响应性时,浅层 API 可以优化性能。
-
其他工具函数:
-
isRef(value)
: 检查一个值是否为 ref 对象。 -
unref(refOrValue)
: 如果参数是 ref,则返回其内部值,否则返回参数本身。等价于isRef(val) ? val.value : val
。 -
toRef(object, key)
: 为响应式对象上的一个属性创建一个 ref。这个 ref 与其源属性保持同步:修改源属性会更新 ref,反之亦然。 -
toRefs(object)
: 将一个响应式对象转换为一个普通对象,其中每个属性都是指向原始对象相应属性的 ref。常用于解构响应式对象而不丢失其响应性。 -
isReactive(value)
: 检查一个对象是否是由reactive
创建的响应式代理。 -
isReadonly(value)
: 检查一个对象是否是由readonly
创建的只读代理。 -
isProxy(value)
: 检查一个对象是否是由reactive
或readonly
创建的代理。 -
toRaw: 返回 reactive 或 readonly 代理的原始对象。用于某些不希望触发响应式追踪/更新的场景。
-
markRaw: 将一个对象标记为不可被代理。即使它被放入响应式对象中,也不会变成响应式的。用于优化性能或处理某些第三方库的对象。
-
effect: 更底层的创建响应式副作用的 API (通常 watch 和 watchEffect 更常用)。
-
自定义 Ref (customRef): 创建自定义行为的 ref,可以显式控制其依赖追踪和更新触发。
-
8. 依赖收集 (track
) 与触发更新 (trigger
) 的内部细节
-
虽然前面提到了这两个过程,但面试中可能会追问具体实现。
-
track(target, type, key)
:target
: 被操作的目标对象。type
: 操作类型(如TrackOpTypes.GET
)。key
: 被访问的属性键。- 当执行
track
时,会检查当前是否有活动的副作用函数 (activeEffect
)。 - 如果有,就将
activeEffect
添加到target
对象的key
属性对应的依赖集合中。
-
trigger(target, type, key, newValue, oldValue)
:target
,key
: 同上。type
: 操作类型(如TriggerOpTypes.SET
,TriggerOpTypes.ADD
)。newValue
,oldValue
: 新旧值。- 当执行
trigger
时,会从依赖存储中找到所有依赖于target
对象的key
属性的副作用函数。 - 然后将这些副作用函数放入一个调度队列中(通常会去重),等待执行。
9. Effect 作用域 (effectScope
)
- 目的 :用于控制响应式副作用 (
effect
) 的生命周期,方便集中创建和管理多个effect
,并在不再需要时统一停止它们,避免内存泄漏。 - 用法 :
- 创建一个
effectScope
实例。 - 调用其
run()
方法,在run
的回调函数内创建的effect
(包括watch
,watchEffect
,computed
的内部 effect) 会被自动收集到该作用域。 - 当调用作用域实例的
stop()
方法时,所有被该作用域收集的effect
都会被停止。
- 创建一个
- 应用场景:在可复用的组合式函数中,或者在需要手动管理多个长期存在的响应式效果的场景下非常有用。
Vue 编译器 (Compiler)
Vue 编译器是 Vue.js 将用户编写的模板 (template) 转换为可执行的渲染函数 (render function) 的核心模块。这个过程发生在构建时 (AOT - Ahead-of-Time compilation,如通过 Vite 或 Vue CLI) 或运行时 (JIT - Just-in-Time compilation,如果直接在浏览器中使用包含编译器的 Vue 版本并传入模板字符串)。理解编译器的工作原理对于深入了解 Vue 的内部机制、模板优化以及自定义构建流程至关重要。
一、编译器的核心职责与目标
- 核心职责 :
- 将用户声明式的模板字符串 或
.vue
文件中的<template>
内容,转换为一个高效的、可执行的 JavaScript 渲染函数。 - 这个渲染函数在执行时会返回一个虚拟 DOM (VNode) 树,供渲染器使用。
- 将用户声明式的模板字符串 或
- 核心目标 :
- 开发者友好: 允许开发者使用直观、类似 HTML 的模板语法来描述 UI。
- 性能优化: 在编译阶段进行尽可能多的分析和优化,以提升运行时的渲染性能。这包括静态内容分析、动态绑定标记等。
- 平台无关性: 虽然编译器的主要目标是生成用于 Web 平台的渲染函数,但其核心解析和转换逻辑可以被抽象。
二、编译器的主要阶段 (Compilation Pipeline)
Vue 编译器的过程可以大致分为三个主要阶段:
- 模板解析 (Parse): 将模板字符串转换为抽象语法树 (AST)。
- 转换/优化 (Transform): 遍历 AST,对其进行各种转换和优化。
- 代码生成 (Generate): 将优化后的 AST 转换为渲染函数的 JavaScript 代码字符串。
三、阶段一:模板解析 (Parse) - 从模板到 AST
此阶段的目标是将原始的模板字符串解析成一个结构化的、易于处理的 JavaScript 对象------模板 AST (Template Abstract Syntax Tree)。
-
词法分析 (Lexical Analysis / Tokenization) - (隐式过程):
- 虽然 Vue 的解析器不像传统编译器那样显式地分为词法分析和语法分析,但其内部逻辑包含了类似的过程。
- 它会逐字符扫描模板字符串,识别出不同的"标记 (tokens)"或有意义的片段,如开始标签 (
<div
), 结束标签 (</div>
), 属性 (id="app"
), 指令 (v-if="show"
), 插值表达式 ({{ message }}
), 文本内容等。
-
语法分析 (Syntax Analysis / Parsing):
- 基于识别出的标记和 Vue 的模板语法规则,构建一个树形结构,即模板 AST。
- AST 节点类型 :
- 元素 (Element Node): 代表 HTML 标签,包含标签名、属性、指令、子节点等。
- 文本 (Text Node): 代表静态文本内容。
- 插值 (Interpolation Node) : 代表
{{ }}
表达式。 - 指令 (Directive Node) : 代表
v-if
,v-for
,v-bind
等指令及其属性值。 - 注释 (Comment Node) : 代表
<!-- -->
。 - 根节点 (Root Node): 整个模板的根。
- 解析过程中的处理 :
- 处理标签的嵌套关系。
- 解析指令和属性。
- 处理特殊标签如
<template>
,<slot>
,<component>
。 - 对 HTML 实体进行解码。
- 报告模板语法错误。
四、阶段二:转换与优化 (Transform) - AST 的增强与标记
在获得初始的模板 AST 后,编译器会对其进行一系列的转换和优化操作。这个阶段的目标是分析 AST,收集信息,并对 AST 进行修改,以便生成更高效的渲染函数。
-
核心转换插件 (Core Transform Plugins):
- Vue 编译器内部有一系列转换插件,每个插件负责处理特定的语法或进行特定的优化。这些插件会遍历 AST,并对匹配到的节点进行操作。
- 常见的转换操作 :
- 指令转换 : 将模板中的指令 (如
v-if
,v-for
,v-model
,v-on
,v-bind
) 转换为 AST 节点上特定的属性或结构,这些结构在代码生成阶段会被用来生成对应的 JavaScript 逻辑。例如,v-if
可能会被转换为一个条件渲染的 JavaScript 结构。 - 表达式分析: 解析指令和插值中的 JavaScript 表达式,识别其依赖的变量,并进行必要的转换 (例如,确保在渲染函数的作用域内正确访问变量)。
slot
处理 : 解析<slot>
标签,为作用域插槽和具名插槽生成相应的数据结构。key
属性处理 : 确保v-for
中的key
属性得到正确处理。- 静态内容分析与标记 : 这是 Vue 3 编译优化的核心之一。
- 静态节点提升 (Static Hoisting): 识别出 AST 中完全静态的节点或属性 (即在渲染过程中永远不会改变的内容)。这些静态部分会被提升到渲染函数之外创建,并在多次渲染中复用,避免了重复创建 VNode 和不必要的 Diff。
- 补丁标记 (Patch Flags) : 针对动态节点,分析其动态绑定的具体类型 (例如,只有文本是动态的、只有 class 是动态的、有动态 key 等),并为 VNode 添加
patchFlag
。渲染器在 Diff 阶段可以利用这些标记进行"靶向更新",大幅减少比较的范围。 - 区块树 (Block Tree) / 动态子节点收集: 将模板分割成"区块",并收集每个区块内所有动态的后代节点,使得更新时只需关注这些动态节点。
- 缓存事件处理函数 (
cacheHandlers
) : 对于内联的事件处理函数,如果开启了缓存,编译器会生成代码将其缓存起来,避免每次渲染都创建新的函数实例 (优化v-on
的性能)。
- 指令转换 : 将模板中的指令 (如
-
转换上下文 (Transform Context):
- 在转换过程中,会维护一个上下文对象,用于在不同的转换插件之间共享信息,例如收集到的静态节点、辅助函数的导入需求等。
五、阶段三:代码生成 (Generate) - 从优化后的 AST 到渲染函数字符串
此阶段的目标是将经过转换和优化的 AST 转换为一个 JavaScript 代码字符串,这个字符串就是最终的渲染函数。
-
代码生成器 (Code Generator):
- 遍历优化后的 AST。
- 根据 AST 节点的类型和转换阶段添加的标记,生成对应的 JavaScript 代码片段。
- 生成 VNode 创建调用 :
- 对于元素节点,会生成
_createElementVNode(...)
(或_createVNode
) 或针对特定优化类型的函数调用 (如_createTextVNode
,_createStaticVNode
)。 - 这些函数的参数包括标签名、属性对象、子节点数组、Patch Flags 等。
- 对于元素节点,会生成
- 处理指令逻辑 :
- 将
v-if
转换为 JavaScript 的三元表达式或if
语句。 - 将
v-for
转换为_renderList(...)
辅助函数的调用,内部会进行循环。 - 将
v-model
转换为对应的value
属性绑定和onInput
(或相应事件) 的事件监听。
- 将
- 处理插值和表达式: 将它们嵌入到生成的代码中,确保能访问到组件实例的数据。
- 引入辅助函数 : 生成代码时可能会依赖一些运行时的辅助函数 (如
_renderList
,_resolveDirective
,_normalizeClass
等),代码生成器会确保这些函数被正确导入或定义。 - 静态提升代码的生成: 将之前标记为静态提升的 VNode 或属性在渲染函数外部声明。
- 作用域处理 : 确保渲染函数内部能正确访问到
this
(在 Options API 中) 或setup
返回的上下文。
-
最终输出:
- 一个包含渲染函数定义的 JavaScript 代码字符串。
- 例如:
function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock(...)) }
(简化示意)。 - 这段代码之后可以通过
new Function(code)
(不推荐,有安全和性能问题) 或者更安全的方式执行,或者在构建时直接写入到.js
文件中。
六、编译器的可配置性与模式
- 编译器选项 (Compiler Options) :
- 可以向编译器传递一些选项来定制其行为,例如:
mode
:'module'
(生成 ES Module 格式) 或'function'
。prefixIdentifiers
: 是否为模板中的表达式变量添加_ctx.
前缀。hoistStatic
: 是否开启静态提升。cacheHandlers
: 是否开启事件处理函数缓存。nodeTransforms
,directiveTransforms
: 允许用户提供自定义的 AST 转换插件。isCustomElement
: 定义哪些标签应被视作自定义元素。
- 可以向编译器传递一些选项来定制其行为,例如:
- 浏览器内编译 vs. 构建时编译 :
- 浏览器内编译 (Runtime + Compiler build): Vue.js 的完整构建版本包含编译器。可以直接在 HTML 中写模板,Vue 会在运行时将其编译成渲染函数。方便,但牺牲了性能(编译耗时)和包体积(编译器代码较大)。
- 构建时编译 (Runtime-only build) : 在开发过程中,通过构建工具 (Vite, Vue CLI) 将
.vue
文件或模板预编译成渲染函数。最终部署到生产环境的是只包含运行时的 Vue 版本,体积更小,性能更好。这是现代 Vue 项目推荐的方式。
好的,我们来逐个深入探讨你提出的这些 Vue 3 核心知识点,力求完整且精简,方便你理解和面试。
Vue 3 核心特性
一、组件的生命周期 (Lifecycle Hooks)
Vue 3 组件的生命周期钩子主要通过 Composition API 中的 onX
系列函数来注册。它们描述了组件从创建到销毁过程中的不同阶段,允许开发者在特定时机执行代码。
Composition API Hook | Options API Equivalent | 执行时机与核心职责 |
---|---|---|
onBeforeMount |
beforeMount |
在组件 DOM 实际挂载到页面之前执行。此时模板已编译,但尚未替换 el 。 |
onMounted |
mounted |
组件 DOM 已经挂载到页面之后执行。此时可以访问和操作 DOM。常用于发起异步请求、设置定时器等。 |
onBeforeUpdate |
beforeUpdate |
当组件依赖的响应式数据发生变化,导致虚拟 DOM 重新渲染之前执行。 |
onUpdated |
updated |
当组件依赖的响应式数据发生变化,导致虚拟 DOM 重新渲染和真实 DOM 更新之后执行。 |
onBeforeUnmount |
beforeUnmount |
在组件实例被卸载之前执行。此时组件实例仍然可用。常用于清理定时器、取消事件监听等。 |
onUnmounted |
unmounted |
组件实例被卸载之后执行。 |
onErrorCaptured |
errorCaptured |
当捕获到来自后代组件的错误时执行。可以返回 false 来阻止错误继续向上冒泡。 |
onRenderTracked |
(无) | (仅限开发模式) 当响应式依赖在渲染过程中被追踪时调用。用于调试。 |
onRenderTriggered |
(无) | (仅限开发模式) 当响应式依赖触发重新渲染时调用。用于调试。 |
onActivated |
activated |
(配合 <keep-alive> ) 当被缓存的组件被激活时调用。 |
onDeactivated |
deactivated |
(配合 <keep-alive> ) 当被缓存的组件失活时调用。 |
核心理解:
setup()
函数在所有这些生命周期钩子之前执行 (逻辑上等同于beforeCreate
和created
)。- Composition API 钩子可以在
setup()
内多次调用,且必须同步调用 (不能在异步回调中注册)。
二、组件通信 (Component Communication)
Vue 3 提供了多种组件间通信的方式,遵循单向数据流原则 (Props down, Events up)。
-
Props (
defineProps
):- 父向子传递数据。
- 子组件通过
defineProps
(在<script setup>
) 声明其接收的 props,可以指定类型、默认值、是否必需和自定义校验。 - Props 是单向绑定的:当父组件的 prop 更新时,会向下流动到子组件,但子组件不应直接修改 prop。
-
Events (
defineEmits
,$emit
):- 子向父传递消息/数据。
- 子组件通过
defineEmits
声明其可能触发的事件。 - 使用
$emit('eventName', ...args)
触发事件,父组件通过@eventName="handler"
监听。
-
v-model
(在组件上):- 语法糖,用于简化父子组件间的双向数据绑定。
- 默认情况下,它会传递一个
modelValue
prop 给子组件,并监听子组件触发的update:modelValue
事件。 - 子组件需要显式地
defineProps(['modelValue'])
和defineEmits(['update:modelValue'])
。 - Vue 3 支持一个组件上有多个
v-model
,例如v-model:title="pageTitle"
。
-
Provide / Inject (
provide
,inject
):- 跨层级组件通信,允许祖先组件向其所有后代组件注入依赖,无论层级多深。
- 祖先组件使用
provide('key', value)
提供数据。 - 后代组件使用
inject('key', defaultValue)
注入数据。 - 可以提供响应式数据,当提供的数据变化时,注入的地方也会更新。
-
Refs (
ref
属性,defineExpose
):- 允许父组件访问子组件的实例或子组件内的 DOM 元素。
- 父组件在子组件标签上使用
ref="childRef"
。 - 在
<script setup>
中,子组件需要通过defineExpose({ exposedMethod, exposedData })
显式暴露希望被父组件访问的属性和方法。
-
$attrs
:- 包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。
- 当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过
v-bind="$attrs"
传入内部组件。 - 在 Vue 3 中,
$attrs
也包含了事件监听器。
三、插槽 (Slots)
插槽是 Vue 组件内容分发 API,允许父组件向子组件的模板中插入自定义内容。
-
默认插槽 (Default Slot):
- 子组件使用
<slot></slot>
。 - 父组件在子组件标签内部直接书写的内容会填充到默认插槽。
- 子组件使用
-
具名插槽 (Named Slots):
- 子组件使用
<slot name="header"></slot>
定义具名插槽。 - 父组件使用
<template v-slot:header>
(简写#header
) 来向特定名称的插槽提供内容。
- 子组件使用
-
作用域插槽 (Scoped Slots):
- 允许子组件在渲染插槽时将数据传递给父组件,让父组件可以根据这些数据来决定插槽内容的渲染方式。
- 子组件:
<slot name="item" :itemData="dataFromChild"></slot>
。 - 父组件:
<template #item="slotProps"> {{ slotProps.itemData }} </template>
。slotProps
是一个包含子组件传递的所有 prop 的对象。
核心理解: 插槽极大地增强了组件的灵活性和可复用性,使得组件可以作为布局框架,具体内容由使用者定义。
四、动态组件 (<component :is="componentName">
)
动态组件允许你根据一个响应式变量的值来动态地切换渲染哪个组件。
- 用法 :
<component :is="currentComponent"></component>
currentComponent
可以是:- 已注册组件的名称 (字符串)。
- 导入的组件选项对象。
- 当
currentComponent
的值改变时,Vue 会卸载旧组件,挂载新组件。 - 常用于实现标签页切换、根据用户角色显示不同视图等场景。
- 可以配合
<keep-alive>
来缓存失活的动态组件。
五、自定义指令 (Custom Directives)
自定义指令允许你封装可复用的底层 DOM 操作逻辑。
- 定义 (全局或局部) :
- 全局 :
app.directive('my-directive', { /* hooks */ })
- 局部 (组件内) : 在
<script setup>
中,任何以v
开头的驼峰式命名的变量都可以被用作一个自定义指令。例如const vFocus = { mounted: el => el.focus() }
,模板中使用<input v-focus />
。
- 全局 :
- 指令钩子函数 (Directive Hooks) : 类似组件生命周期,提供了在指令绑定到元素的不同阶段执行逻辑的能力。
created(el, binding, vnode, prevVnode)
: 在元素的 attribute 或事件监听器被应用之前调用。beforeMount(el, binding, vnode, prevVnode)
: 当指令第一次绑定到元素并且在挂载父组件之前调用。mounted(el, binding, vnode, prevVnode)
: 在绑定元素的父组件被挂载后调用。beforeUpdate(el, binding, vnode, prevVnode)
: 在元素本身更新之前调用。updated(el, binding, vnode, prevVnode)
: 在元素本身及其子树完成更新后调用。beforeUnmount(el, binding, vnode, prevVnode)
: 在卸载绑定元素的父组件之前调用。unmounted(el, binding, vnode, prevVnode)
: 当指令与元素解除绑定且父组件已卸载时调用。
binding
对象 : 包含了指令的值 (value
)、参数 (arg
)、修饰符 (modifiers
) 等信息。
核心理解: 自定义指令是对底层 DOM 操作的抽象,适用于那些不适合通过组件复用的、直接操作 DOM 的场景。
六、双向绑定 (v-model
) 是如何实现的?
v-model
是一个语法糖,其实现原理取决于它用在什么类型的元素上:
-
用在原生 HTML 表单元素上 (如
<input>
,<textarea>
,<select>
):- 对于文本输入框 (
<input type="text">
,<textarea>
) :- 它会绑定元素的
value
property (通过v-bind:value
)。 - 它会监听元素的
input
DOM 事件 (通过v-on:input
),并在事件回调中将输入的值赋给v-model
绑定的变量。
- 它会绑定元素的
- 对于复选框 (
<input type="checkbox">
) :- 绑定
checked
property。 - 监听
change
DOM 事件。
- 绑定
- 对于单选按钮 (
<input type="radio">
) :- 绑定
checked
property (如果value
attribute 匹配)。 - 监听
change
DOM 事件。
- 绑定
- 对于选择框 (
<select>
) :- 绑定
value
property。 - 监听
change
DOM 事件。
- 绑定
- Vue 内部会根据不同的输入类型应用不同的 DOM property 和事件。
- 对于文本输入框 (
-
用在自定义组件上:
- 默认情况下,
v-model
等价于:- 传递一个名为
modelValue
的 prop 给子组件::modelValue="variable"
- 监听子组件触发的一个名为
update:modelValue
的事件:@update:modelValue="variable = $event"
- 传递一个名为
- 子组件需要:
- 通过
defineProps(['modelValue'])
接收modelValue
prop。 - 通过
defineEmits(['update:modelValue'])
声明会触发update:modelValue
事件。 - 在内部适当的时机 (例如,当内部表单元素值变化时),调用
$emit('update:modelValue', newValue)
来通知父组件更新。
- 通过
- 自定义
v-model
参数 : Vue 3 允许自定义v-model
的 prop 名称和事件名称,例如v-model:title="pageTitle"
会传递title
prop 并监听update:title
事件。
- 默认情况下,
核心理解 : v-model
本质上是属性绑定和事件监听的组合,实现了数据在父子组件或视图与数据间的双向同步。
七、内置组件:Transition
是如何实现的?
<Transition>
组件用于给单个元素或组件的进入和离开添加过渡效果。
- 原理 :
<Transition>
组件本身不渲染任何额外的 DOM 元素,它是一个抽象组件。- 它监听其唯一的直接子元素或组件 的条件渲染 (由
v-if
,v-show
或动态组件切换触发) 或创建/销毁。
- 工作流程 :
- 进入 (Enter) :
- 当子元素插入 DOM 时,Vue 会在下一帧应用
v-enter-from
class (定义初始状态) 和v-enter-active
class (定义过渡的持续时间、缓动曲线等)。 - 再下一帧,Vue 会移除
v-enter-from
class,并应用v-enter-to
class (定义结束状态)。浏览器会根据v-enter-active
中定义的 CSS transition 或 animation 来执行动画。 - 过渡结束后 (通过监听
transitionend
或animationend
事件),Vue 会移除v-enter-to
和v-enter-active
class。
- 当子元素插入 DOM 时,Vue 会在下一帧应用
- 离开 (Leave) :
- 当子元素将要从 DOM 移除时,Vue 会立即应用
v-leave-from
class 和v-leave-active
class。 - 再下一帧 (或同步,取决于具体实现和模式),应用
v-leave-to
class,同时移除v-leave-from
。 - 过渡结束后,Vue 会移除
v-leave-to
和v-leave-active
class,并将元素从 DOM 中实际移除。
- 当子元素将要从 DOM 移除时,Vue 会立即应用
- 进入 (Enter) :
- CSS 类名 :
v-enter-from
/name-enter-from
v-enter-active
/name-enter-active
v-enter-to
/name-enter-to
v-leave-from
/name-leave-from
v-leave-active
/name-leave-active
v-leave-to
/name-leave-to
(如果<Transition>
组件有name
prop,则v-
会被替换为name-
)
- JavaScript 钩子 :
<Transition>
也提供了 JavaScript 钩子 (如@before-enter
,@enter
,@after-enter
等),允许通过 JavaScript 直接操作 DOM 来实现更复杂的动画。 - 模式 (Modes) :
in-out
(新元素先进入,然后当前元素离开),out-in
(当前元素先离开,然后新元素进入)。
核心理解 : <Transition>
通过在元素进入/离开的不同阶段动态添加/移除 CSS 类名,并利用浏览器的 CSS Transitions 或 Animations 来实现动画效果。
八、内置组件:KeepAlive
保活的原理
<KeepAlive>
组件用于缓存失活的动态组件或普通组件实例,而不是销毁它们。
- 原理 :
<KeepAlive>
也是一个抽象组件,不渲染额外的 DOM。- 它包裹动态切换的组件 (通常是
<component :is="...">
或<router-view>
的子组件)。
- 工作流程 :
- 缓存 : 当被
<KeepAlive>
包裹的组件因为条件不再满足 (如v-if
变为false
或动态组件切换) 而失活时,<KeepAlive>
不会卸载该组件实例 ,而是将其移出当前的 DOM 树 ,并将其 VNode 和组件实例缓存 在一个内部对象中 (通常是一个以key
或组件类型为键的 Map)。 - 激活 : 当这个组件再次需要被渲染时 (条件满足或切换回来),
<KeepAlive>
会从缓存中取出对应的 VNode 和组件实例,然后将其重新插入到 DOM 树中,而不是创建一个新的实例。
- 缓存 : 当被
- 生命周期钩子 :
onActivated()
: 当被缓存的组件被重新激活并插入 DOM 时调用。onDeactivated()
: 当组件失活并从 DOM 中移除但被缓存时调用。 (这些钩子只对<KeepAlive>
的直接子组件有效)
- Props :
include
: 字符串或正则表达式。只有名称匹配的组件会被缓存。exclude
: 字符串或正则表达式。任何名称匹配的组件都不会被缓存。max
: 数字。最多可以缓存多少组件实例。当超出数量时,会采用 LRU (Least Recently Used) 策略移除最久未被访问的缓存。
核心理解 : <KeepAlive>
通过将失活的组件实例保存在内存中,并在需要时重新将其插入 DOM,从而避免了组件的重复创建和销毁,保留了组件的状态,提升了性能。
九、内置组件:Teleport
是如何实现选择性挂载的?
<Teleport>
组件允许你将模板的一部分"传送"到 DOM 树中的另一个位置进行渲染,即使这个位置不在当前组件的挂载点下。
- 原理 :
<Teleport>
组件接收一个to
prop,该 prop 指定了目标 DOM 元素 (可以是一个 CSS 选择器字符串,或者一个真实的 DOM 节点)。- 其插槽内容 (即
<Teleport>
标签内部的内容) 会被渲染成 VNode。 - Vue 的渲染器在处理
<Teleport>
组件时,不会将这些 VNode 挂载到<Teleport>
组件在父组件模板中的位置。 - 而是,它会获取
to
prop 指定的目标 DOM 元素,然后将插槽内容的 VNode 挂载到这个目标 DOM 元素下。
- 逻辑连接 :
- 尽管
<Teleport>
的内容在 DOM 结构上被移动了,但它在 Vue 的组件树中的逻辑父子关系保持不变。 - 这意味着,从
<Teleport>
内容内部通过inject
仍然可以访问其逻辑父组件provide
的数据。 - Props 和事件的传递也遵循其逻辑父子关系。
- 尽管
- Props :
to
: (必需) CSS 选择器字符串或 DOM 元素,指定内容传送的目标。disabled
: (可选) 布尔值。如果为true
,内容将不会被传送,而是渲染在<Teleport>
组件在模板中的原始位置。
核心理解 : <Teleport>
利用渲染器的能力,在 VNode 挂载阶段将其子 VNode 实际插入到由 to
prop 指定的 DOM 节点下,同时维持其在组件树中的逻辑层级关系。常用于实现 Modals, Notifications, Dropdowns 等需要脱离当前组件 DOM 结构渲染的 UI。
十、内置组件:Suspense
原理与异步
<Suspense>
组件用于优雅地处理组件树中的异步依赖,通常是异步组件或带有 async setup()
的组件。
- 原理 :
<Suspense>
组件有两个插槽:#default
和#fallback
。- 它会尝试渲染
#default
插槽中的内容。
- 工作流程 :
- 异步依赖解析 :
- 如果
#default
插槽中的组件是一个异步组件 (defineAsyncComponent
) 或其setup()
函数返回一个 Promise (即async setup()
),<Suspense>
会等待这个异步操作完成。 - 在等待期间 (即 Promise 处于 pending 状态),
<Suspense>
会显示#fallback
插槽中的内容 (通常是一个加载指示器)。
- 如果
- 依赖解析完成 :
- 当
#default
插槽中所有深层嵌套的异步依赖都成功解析 (Promise resolved) 后,<Suspense>
会切换回显示#default
插槽的内容。
- 当
- 错误处理 :
- 如果任何异步依赖解析失败 (Promise rejected),错误会向上冒泡,可以被
onErrorCaptured
钩子或更上层的<Suspense>
(如果嵌套) 或错误边界捕获。
- 如果任何异步依赖解析失败 (Promise rejected),错误会向上冒泡,可以被
- 异步依赖解析 :
- 嵌套
Suspense
:<Suspense>
组件可以嵌套。内部的<Suspense>
会优先处理其自身的异步依赖。 - 与
<Transition>
结合 : 可以与<Transition>
结合使用,为异步内容的加载和切换添加动画效果。
核心理解 : <Suspense>
通过捕获其 #default
插槽内组件的异步状态 (Promise),在异步操作完成前渲染 #fallback
内容,完成后再渲染 #default
内容,从而提供了一种声明式的方式来处理异步加载的用户体验。