Vue 3 核心响应式与渲染原理全景拆解
文章目录
- Vue 3 核心响应式与渲染原理全景拆解
- 1. 响应式基石:`Proxy` 与 `Reflect`
- 为什么 `Proxy` 优于 `Object.defineProperty`?
- 关键代码逻辑(简化版)
- 2. 依赖收集核心:`Effect` 与全景 `targetMap` 映射表
- 依赖关系映射结构(`targetMap`)
- 3. `ref` 与 `reactive` 原理拆解
- `reactive`
- `ref`
- 4. 渲染与 Diff 优化:编译时与运行时的高效协同
- ① Block Tree 与动态节点标记(`PatchFlag`)
- ② 最长递增子序列算法(Fast Diff)
- 5. 组件级渲染机制与异步更新队列
- 异步队列(`queueJob`)与 `nextTick`
- 6. Vue 2 vs Vue 3 全景架构对比总结
Vue 3 核心响应式与渲染原理全景拆解
如果说 Vue 2 的响应式是一套设计精巧的“面向对象类系统(Watcher / Dep)”,那么 Vue 3 则彻底演变为一套基于 ES6+ 现代特性的“高并发、轻量化、函数式系统(Proxy / Effect)”。
1. 响应式基石:Proxy与Reflect
Vue 3 抛弃了Object.defineProperty,转而使用 ES6 的Proxy(代理)配合Reflect(反射)实现数据的响应式劫持。
为什么Proxy优于Object.defineProperty?
| 维度 | Vue 2 (Object.defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 劫持对象 | 属性级别(需要递归遍历每一个 key) | 对象级别(直接代理整个对象) |
| 新增/删除属性 | 无法侦测,需借助this.$set/$delete | 天然支持(set/deleteProperty拦截) |
| 数组支持 | 依赖拦截重写 7 个变异方法,无法感知下标修改 | 天然支持(正常拦截数组索引修改与length变化) |
| 初始化性能 | 递归初始化所有属性,层级越深越慢 | 按需惰性代理(只有读取到深层对象时才递归创建reactive) |
关键代码逻辑(简化版)
functionreactive(target){if(typeoftarget!=='object'||target===null)returntarget;returnnewProxy(target,{get(target,key,receiver){// 1. 依赖收集 (track)track(target,key);constres=Reflect.get(target,key,receiver);// 2. 惰性响应式:取值时发现是对象,才进行深层代理returntypeofres==='object'&&res!==null?reactive(res):res;},set(target,key,value,receiver){constoldValue=target[key];constresult=Reflect.set(target,key,value,receiver);// 3. 值发生变化时派发更新 (trigger)if(hasChanged(value,oldValue)){trigger(target,key);}returnresult;}});}2. 依赖收集核心:Effect与全景targetMap映射表
Vue 3 废弃了 Vue 2 的Dep类,引入了全局统一的双重 WeakMap / Map / Set 依赖数据结构(targetMap)和底层执行单元Effect(作用/副作用)。
依赖关系映射结构(targetMap)
在内存中,响应式依赖通过如下关系精确绑定:
targetMap (WeakMap) └── target 对象 (Key) └── depsMap (Map) └── key 属性 (Key) └── dep (Set: 存放所有依赖该属性的 ReactiveEffect)// 全局依赖树consttargetMap=newWeakMap();letactiveEffect=null;// 当前正在运行的 Effect// 1. 依赖收集:trackfunctiontrack(target,key){if(!activeEffect)return;letdepsMap=targetMap.get(target);if(!depsMap){targetMap.set(target,(depsMap=newMap()));}letdep=depsMap.get(key);if(!dep){depsMap.set(key,(dep=newSet()));}// 将当前副作用添加进 Set 集合(自动去重)dep.add(activeEffect);}// 2. 派发更新:triggerfunctiontrigger(target,key){constdepsMap=targetMap.get(target);if(!depsMap)return;constdep=depsMap.get(key);if(dep){// 拷贝一份防止无限循环,依次执行副作用consteffectsToRun=newSet(dep);effectsToRun.forEach(effect=>{if(effect.scheduler){effect.scheduler();// 如果有调度器(如组件渲染队列、computed 脏标记),优先走调度器}else{effect.run();// 否则直接运行}});}}3.ref与reactive原理拆解
Vue 3 提供了两种主要的响应式声明 API:reactive和ref。
reactive
- 作用对象:仅接收复杂数据类型(Object / Array / Map / Set)。
- 实现原理:底层基于 ES6
Proxy对对象进行全方位包装。
ref
- 作用对象:既可以接收原始值(String / Number / Boolean),也可以接收对象。
- 实现原理:
- 由于 ES6
Proxy无法直接代理原始值,Vue 3 包装了一个RefImpl类; - 利用
getter/setter(属性访问器)拦截.value的读写动作; - 如果
ref传入的是对象,底层会自动调用reactive转为Proxy。
classRefImpl{constructor(value){this._rawValue=value;// 如果是对象则调用 reactive 代理,否则返回原值this._value=isObject(value)?reactive(value):value;this.__v_isRef=true;// 属性标记,用于自动解包}getvalue(){trackRefValue(this);// 手动触发 ref 的依赖收集returnthis._value;}setvalue(newVal){if(hasChanged(newVal,this._rawValue)){this._rawValue=newVal;this._value=isObject(newVal)?reactive(newVal):newVal;triggerRefValue(this);// 触发更新}}}4. 渲染与 Diff 优化:编译时与运行时的高效协同
Vue 3 不仅重构了响应式,还大幅优化了编译(Compiler)与运行时(Runtime)的配合,实现了极其高效的 DOM Diff。
① Block Tree 与动态节点标记(PatchFlag)
Vue 2 在数据更新时,需要逐层对整棵虚拟 DOM 树(VNode)进行递归对比。
Vue 3 引入了静态提升(Static Hoisting)和动态节点标记(PatchFlag):
- 动态节点(Dynamic Nodes):编译阶段分析出哪些节点带数据绑定(如
:class="cls"或\{\{ text \}\}),并赋予对应的位掩码PatchFlag(如1代表仅动态文本,2代表仅动态 class)。 - Block 容器:根节点将所有动态子节点收集到一个一维数组
dynamicChildren中。 - 更新阶段:Diff 过程跳过所有静态节点,直接遍历
dynamicChildren数组,并且只对比PatchFlag指定的属性(如只比对 class,不比对属性与文本)。
② 最长递增子序列算法(Fast Diff)
在列表 Diff 阶段(如v-for顺序大洗牌),Vue 2 使用双端对比算法(Four-pointer Diff),而 Vue 3 采用了效率更高的Fast Diff 算法:
- 预处理同步头尾:先分别从头、从尾比对相同节点,直接复用并挂载。
- 构建映射表:对中间剩余的乱序子节点建立新旧索引映射。
- 最长递增子序列(LIS):求解旧节点在新列表中位置的最长递增子序列。子序列中的节点意味着“相对顺序未变”,在 DOM 更新时完全不需要移动,从而将 DOM 移动操作降至最低。
5. 组件级渲染机制与异步更新队列
在 Vue 3 中,每个组件实例在挂载(mount)时,都会创建一个专门负责渲染的 ReactiveEffect:
// 组件渲染逻辑简化constsetupRenderEffect=(instance,container)=>{constcomponentUpdateFn=()=>{if(!instance.isMounted){// 1. 首次渲染:执行 render 生成 VNode -> 挂载 DOMconstsubTree=(instance.subTree=instance.render.call(instance.proxy));patch(null,subTree,container);instance.isMounted=true;}else{// 2. 重新渲染:生成新 VNode -> 比对 Diff -> 更新 DOMconstnextTree=instance.render.call(instance.proxy);constprevTree=instance.subTree;instance.subTree=nextTree;patch(prevTree,nextTree,container);}};// 创建渲染 Effect,并传入 scheduler 实现批量异步更新consteffect=(instance.effect=newReactiveEffect(componentUpdateFn,()=>queueJob(update)// 数据变化时不立即计算,而是入队微任务));constupdate=(instance.update=()=>effect.run());update();};异步队列(queueJob)与nextTick
- 当修改多个
ref或reactive属性时,trigger会触发effect.scheduler(),将更新任务推进全局的queue调度队列。 - 队列通过
Promise.resolve().then(flushJobs)在微任务中统一去重并执行(按组件创建顺序自顶向下刷新),保证组件在同一 Tick 内只会重新 Render 一次。
6. Vue 2 vs Vue 3 全景架构对比总结
| 架构模块 | Vue 2 底层方案 | Vue 3 底层方案 |
|---|---|---|
| 响应式核心 | Object.defineProperty(Getter/Setter) | ES6Proxy+Reflect |
| 依赖管理结构 | 闭包内的Dep实例 +Watcher依赖数组 | 全局targetMap(WeakMap) +ReactiveEffect(Set) |
| 计算属性原理 | Watcher(lazy: true) +dirty标记 | ComputedRefImpl+ReactiveEffect链式调度 |
| 侦听器原理 | User Watcher+ 递归traverse()依赖收集 | watch/watchEffect+onCleanup副作用清理 |
| 渲染比对 (Diff) | 树形全量对比(双端 Diff 算法) | Block Tree +PatchFlag靶向比对 +最长递增子序列 |
| 内存开销 | 较重(大量的组件级 / 属性级 Watcher 实例化) | 极轻(无类实例化开销,结构扁平) |
🚀 感谢阅读!想了解更多?
📖 我的博客网站 | 记录思考,分享干货
🏡 我的个人主页 | 关于我、开源项目