目录
一、Proxy 代理
Vue 3 的响应式系统基于 JavaScript 的 Proxy 对象实现,这取代了 Vue 2 中的 Object.defineProperty 方法。Proxy 提供了更强大的拦截能力,可以捕获对象的各种操作。
1. Proxy 工作原理
-
创建代理对象:
- 当使用
reactive()
函数包装一个普通对象时,Vue 会创建一个 Proxy 代理对象 - 这个代理对象拦截所有对原始对象的访问和修改操作
- 当使用
-
拦截读取操作(get):
- 当访问对象的属性时,Proxy 的 get 拦截器会被触发
- Vue 在这个阶段执行依赖收集,记录当前正在运行的代码(称为"副作用函数")与该属性的关系
- 如果访问的值是对象,Vue 会递归地将其转换为响应式对象
-
拦截写入操作(set):
- 当修改对象的属性时,Proxy 的 set 拦截器会被触发
- Vue 检查新值是否与旧值不同
- 如果值发生变化,Vue 会触发更新,通知所有依赖该属性的代码重新执行
2. 依赖收集系统
Vue 维护了一个精密的依赖追踪系统,其核心是三层数据结构:
三级数据结构:
TargetMap
(WeakMap):原始对象 → DepsMapDepsMap
(Map):属性名 → Dep SetDep
(Set):存储依赖该属性的副作用函数
工作流程:
访问属性时(get):调用 track(target, key)
,获取当前运行的副作用函数 activeEffect
,存入对应属性的 Dep Set。
修改属性时(set):调用 trigger(target, key)
,查找 Dep Set 中的所有函数,批量执行这些函数。
核心作用:
- 精确追踪:只有实际被模板/计算属性使用的属性才会建立依赖
- 按需转换:避免无谓的深度遍历,只有访问到的属性才会递归转换
- 性能优化:不需要像 Vue 2 那样递归遍历整个对象初始化
总结:当属性被访问时,Vue 会将当前运行的副作用函数添加到对应属性的 Dep Set 中。当属性值变化时,Vue 会遍历执行该 Set 中的所有函数。
3. ref 的实现原理
ref
主要用于处理基本类型值(如数字、字符串)的响应式,其实现原理与 reactive
不同:
-
值包装:
ref
将基本类型值包装在一个对象中,该对象有一个value
属性,例如:ref(0)
返回{ value: 0 }。
-
响应式机制:使用类的 getter/setter 实现响应式,访问
ref.value
时触发 getter,进行依赖收集,修改ref.value
时触发 setter,检查值变化并触发更新。 -
对象处理:如果
ref
的值是对象,Vue 会自动将其转换为reactive
代理,这样嵌套对象也能保持响应性。 -
模板中的特殊处理:在模板中使用 ref 时,Vue 会自动解包,无需使用
.value,
但在 JavaScript 逻辑中仍需使用.value
访问。
4. reactive 的实现原理
reactive
专门用于处理对象类型的响应式:
-
Proxy 代理:创建原始对象的 Proxy 代理,拦截所有读取和写入操作。
-
深度响应:当访问嵌套对象属性时,Vue 会递归地将其转换为响应式对象。这种转换是惰性的,只有在属性被访问时才会发生。
-
缓存机制:Vue 维护一个 WeakMap 缓存,避免对同一个对象重复创建代理
-
集合类型处理:对 Map、Set 等集合类型有特殊处理,确保其方法(如 add、delete)也能触发响应。
5. 副作用系统(Effect)
Vue 的响应式核心是副作用管理系统。在Vue中,每个组件的渲染过程就是一个主要的副作用,它会修改DOM,改变用户看到的界面。
想象一下,当响应式数据变化时,我们需要知道哪些代码需要重新运行,然后自动触发这些代码运行,还要避免不必要的重复运行。
这就是副作用系统要解决的核心问题。
副作用系统的工作原理:
1. 注册副作用函数
当定义组件、计算属性或侦听器时,Vue内部会创建一个副作用函数:
// 组件本质就是一个副作用函数
function renderComponent() {
// 在这里访问响应式数据
document.getElementById('app').innerHTML = `
<div>Count: ${state.count}</div>
`
}
2. 依赖收集关键
当Vue首次运行这个函数时:
- 设置全局标记
activeEffect = renderComponent
- 执行函数内容
- 访问
state.count
→ 触发getter - getter内部:
track(state, 'count', activeEffect)
- 建立依赖关系:
state.count
→renderComponent
- 清除标记
activeEffect = null
这就建立了"state.count
变化时,要运行renderComponent
"的关系。
3. 触发更新
当修改响应式数据:state.count = 10 → 触发getter
setter内部:trigger(state, 'count')
触发过程:
- 查找所有依赖
state.count
的副作用函数 - 不立即执行,而是将它们放入队列
- 等待当前代码执行完成
- 在微任务中批量执行队列中的所有函数
6. 响应式系统的优势
-
全能力拦截:Proxy 可以拦截更多操作(属性添加/删除、数组索引变化等),解决了 Vue 2 中无法检测属性添加/删除的限制。
-
惰性响应:只有被实际访问的属性才会被转换为响应式,减少了不必要的性能开销。
-
更好的性能:Proxy 是浏览器原生实现,比 defineProperty 更高效,依赖收集更精确,减少了不必要的更新。
-
更丰富的 API:提供 shallowReactive(浅层响应), readonly(只读响应), markRaw(标记非响应)。
7. 响应式流程总结
- 创建响应式对象(reactive/ref)
- 在副作用函数中访问响应式数据
- 触发 get 拦截器,收集当前副作用作为依赖
- 修改响应式数据
- 触发 set 拦截器,检查值变化
- 通知所有相关依赖(副作用函数)更新
- 执行更新,重新渲染视图或执行逻辑
二、组件触发渲染的时机
Vue 3 组件会在以下4种情况下触发重新渲染:
1. 首次挂载时
- 当组件首次被添加到 DOM 中
- 创建组件实例 → 初始化数据 → 执行首次渲染
2. 响应式数据变更
- 当组件依赖的 reactive/ref 数据发生变化
3. Props 更新
- 父组件传递的新 props 与当前值不同
- 更新流程:父组件更新props → 子组件触发beforeUpdate → 子组件重新渲染
4. 强制更新
- 使用
forceUpdate()
方法强制重新渲染 - 应避免使用,只有当响应式数据变更未被检测到时使用
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
instance.proxy.forceUpdate() // 强制重新渲染
三、Vue 3 组件生命周期流程
一、创建阶段(组件实例初始化)
1. 触发时机
- 父组件渲染时:当父组件执行渲染函数遇到子组件标签时,父组件执行渲染函数,遇到子组件的标签(如 <ChildComponent />),创建子组件实例
- 动态组件切换:使用
<component :is="组件名">
时切换组件 - 编程式创建:通过
createApp()
和mount()
手动创建
2. 创建流程
- 初始化实例:创建组件实例对象,分配唯一ID
- 处理Props/Attrs:解析父组件传递的属性和非props属性
- 执行setup():组合式API入口,创建响应式数据,声明计算属性/方法,返回模板可用内容
- 解决依赖关系:处理provide/inject依赖注入,建立父子关联
关键点:此时组件数据和方法已初始化,但尚未生成DOM元素
二、挂载阶段(DOM插入页面)
1. 挂载前准备
- 编译模板:将模板编译为渲染函数
- 创建虚拟DOM:执行渲染函数生成虚拟节点树
- 触发beforeMount:DOM创建前的最后准备阶段
2. 挂载过程
- 生成真实DOM:将虚拟DOM转换为浏览器可识别的元素
- 插入页面:将创建的元素添加到父容器中
- 触发mounted:组件完成挂载,可安全操作DOM
父子顺序:子组件先挂载,父组件后挂载
三、更新阶段(响应数据变化)
1. 触发时机
- 数据变更:组件内部响应式状态变化
- Props更新:父组件传递新的props值
- 插槽变化:父组件中插槽内容更新
- 强制更新:调用
forceUpdate()
方法
2. 更新流程
- 检测变化:Vue 的响应式系统检测到数据变化,标记组件为需要更新状态(dirty)
- 触发beforeUpdate:组件状态已更新但DOM未渲染,可访问更新后的数据但DOM未改变
- 重新渲染:执行渲染函数生成新虚拟DOM,Diff算法对比新旧虚拟DOM差异
- 应用更新:仅修改实际变化的DOM元素,批量处理多个变更以提高性能
- 触发updated:DOM更新完成,可安全操作更新后的DOM元素
父子顺序:父组件先更新,子组件后更新
四、销毁阶段(移除组件)
1. 触发时机
- 条件渲染消失:
v-if
变为false
时销毁 - 动态组件切换:切换到其他组件时,销毁当前显示的组件
- 路由离开:路由切换到其他页面时,销毁当前路由对应的组件
- 父组件销毁:父组件被销毁,所有子组件也随之销毁
- 编程式卸载:调用
unmount()
方法销毁组件
2. 销毁流程
- 触发beforeUnmount:组件即将被销毁但功能仍完整,最后一次访问组件状态和DOM
- 停止响应式:移除所有响应式依赖追踪,停止 computed、watch 等响应式功能
- 清理资源:自动移除模板中注册的事件监听,手动清除清除通过
addEventListener
添加的监听器、定时器、订阅等(需在钩子中实现) - 移除DOM:从DOM树中删除组件元素
- 触发unmounted:组件完成卸载,解除所有引用关系,允许JavaScript垃圾回收器回收内存
父子顺序:子组件先销毁,父组件后销毁
四、虚拟 DOM 和 Diff 算法
1. 初始渲染阶段:
当组件首次渲染时,Vue 执行render
函数生成虚拟 DOM 树,这个虚拟 DOM 是真实 DOM 的轻量级 JavaScript 对象表示。
2. 响应式数据变更触发:
当响应式数据变化时(通过前面提到的依赖收集机制),触发组件重新渲染,Vue 调度器会安排异步更新任务。
3. 生成新虚拟 DOM:
重新执行组件的render
函数,生成新的虚拟 DOM 树。
4. Diff 算法执行:
Vue 调用patch
函数,对比新旧两棵虚拟 DOM 树(核心阶段)
Diff 算法逐层比较节点变化:
先比较根节点类型变化,通过key
值判断是否可复用相同类型节点,检测并更新变化的属性/事件;
采用双端比较算法优化子节点更新,查找可复用的相同节点,计算节点顺序变化的最小操作集。
5. 最小化 DOM 操作:
Diff 算法计算出需要更新的最小节点集合,避免整棵树重渲染,仅更新实际变化的节点。
6. 应用变更:
根据 Diff 结果执行精准的 DOM 操作:创建新节点;移除废弃节点;更新变化节点的属性/内容;调整节点位置。
7. 完成更新:
新虚拟 DOM 树成为当前状态基准,等待下次数据变化触发新一轮更新周期。
关键点:虚拟 DOM 提供抽象层,让 Vue 无需直接操作真实 DOM;Diff 算法实现高效更新,通过对比找出最小变更集,避免整棵树重绘,大幅提升性能。这种机制在组件更新期间始终运行,是 Vue 响应式系统的核心优化手段。