Vue3 响应式原理

目录

一、Proxy 代理

1. Proxy 工作原理

2. 依赖收集系统

3. ref 的实现原理

4. reactive 的实现原理

5. 副作用系统(Effect)

6. 响应式系统的优势

7. 响应式流程总结

二、组件触发渲染的时机

1. 首次挂载时

2. 响应式数据变更

3. Props 更新

4. 强制更新

三、Vue 3 组件生命周期流程

一、创建阶段(组件实例初始化)

二、挂载阶段(DOM插入页面)

三、更新阶段(响应数据变化)

四、销毁阶段(移除组件)

四、虚拟 DOM 和 Diff 算法​​


一、Proxy 代理

Vue 3 的响应式系统基于 JavaScript 的 ​​Proxy​​ 对象实现,这取代了 Vue 2 中的 Object.defineProperty 方法。Proxy 提供了更强大的拦截能力,可以捕获对象的各种操作。

1. Proxy 工作原理

  1. ​创建代理对象​​:

    • 当使用 reactive() 函数包装一个普通对象时,Vue 会创建一个 Proxy 代理对象
    • 这个代理对象拦截所有对原始对象的访问和修改操作
  2. ​拦截读取操作(get)​​:

    • 当访问对象的属性时,Proxy 的 get 拦截器会被触发
    • Vue 在这个阶段执行​​依赖收集​​,记录当前正在运行的代码(称为"副作用函数")与该属性的关系
    • 如果访问的值是对象,Vue 会递归地将其转换为响应式对象
  3. ​拦截写入操作(set)​​:

    • 当修改对象的属性时,Proxy 的 set 拦截器会被触发
    • Vue 检查新值是否与旧值不同
    • 如果值发生变化,Vue 会​​触发更新​​,通知所有依赖该属性的代码重新执行

2. 依赖收集系统

Vue 维护了一个精密的依赖追踪系统,其核心是三层数据结构:

​三级数据结构​​:

  • TargetMap(WeakMap):原始对象 → DepsMap
  • DepsMap(Map):属性名 → Dep Set
  • Dep(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 不同:

  1. ​值包装​​:ref 将基本类型值包装在一个对象中,该对象有一个 value 属性,例如:ref(0) 返回 { value: 0 }。

  2. ​响应式机制​​:使用类的 getter/setter 实现响应式,访问 ref.value 时触发 getter,进行依赖收集,修改 ref.value 时触发 setter,检查值变化并触发更新。

  3. ​对象处理​​:如果 ref 的值是对象,Vue 会自动将其转换为 reactive 代理,这样嵌套对象也能保持响应性。

  4. ​模板中的特殊处理​​:在模板中使用 ref 时,Vue 会自动解包,无需使用 .value,但在 JavaScript 逻辑中仍需使用 .value 访问。

4. reactive 的实现原理

reactive 专门用于处理对象类型的响应式:

  1. ​Proxy 代理​​:创建原始对象的 Proxy 代理,拦截所有读取和写入操作。

  2. ​深度响应​​:当访问嵌套对象属性时,Vue 会递归地将其转换为响应式对象。这种转换是惰性的,只有在属性被访问时才会发生。

  3. ​缓存机制​​:Vue 维护一个 WeakMap 缓存,避免对同一个对象重复创建代理

  4. ​集合类型处理​​:对 Map、Set 等集合类型有特殊处理,确保其方法(如 add、delete)也能触发响应。


5. 副作用系统(Effect)

Vue 的响应式核心是副作用管理系统。在Vue中,​每个组件的渲染过程就是一个主要的副作用它会修改DOM,改变用户看到的界面。

想象一下,当响应式数据变化时,我们需要知道​哪些代码需要重新运行​,然后自动触发​这些代码运行,还要避免​不必要的重复运行​

这就是副作用系统要解决的核心问题。

副作用系统的工作原理:

1. 注册副作用函数

当定义组件、计算属性或侦听器时,Vue内部会创建一个​​副作用函数​​:

// 组件本质就是一个副作用函数
function renderComponent() {
  // 在这里访问响应式数据
  document.getElementById('app').innerHTML = `
    <div>Count: ${state.count}</div>
  `
}

2. 依赖收集关键

当Vue首次运行这个函数时:

  1. 设置全局标记 activeEffect = renderComponent
  2. 执行函数内容
  3. 访问 state.count → 触发getter
  4. getter内部:track(state, 'count', activeEffect)
  5. 建立依赖关系:state.count → renderComponent
  6. 清除标记 activeEffect = null

这就建立了"state.count变化时,要运行renderComponent"的关系。

3. 触发更新

当修改响应式数据:state.count = 10 → 触发getter

setter内部:trigger(state, 'count')

触发过程:

  1. 查找所有依赖 state.count 的副作用函数
  2. ​不立即执行​​,而是将它们放入队列
  3. 等待当前代码执行完成
  4. 在微任务中批量执行队列中的所有函数

6. 响应式系统的优势

  1. ​全能力拦截​​:Proxy 可以拦截更多操作(属性添加/删除、数组索引变化等),解决了 Vue 2 中无法检测属性添加/删除的限制。

  2. ​惰性响应​​:只有被实际访问的属性才会被转换为响应式,减少了不必要的性能开销。

  3. ​更好的性能​​:Proxy 是浏览器原生实现,比 defineProperty 更高效,依赖收集更精确,减少了不必要的更新。

  4. ​更丰富的 API​​:提供 shallowReactive(浅层响应), readonly(只读响应), markRaw(标记非响应)。


7. 响应式流程总结

  1. 创建响应式对象(reactive/ref)
  2. 在副作用函数中访问响应式数据
  3. 触发 get 拦截器,收集当前副作用作为依赖
  4. 修改响应式数据
  5. 触发 set 拦截器,检查值变化
  6. 通知所有相关依赖(副作用函数)更新
  7. 执行更新,重新渲染视图或执行逻辑

   

二、组件触发渲染的时机

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. 创建流程

  1. ​初始化实例​​:创建组件实例对象,分配唯一ID
  2. ​处理Props/Attrs​​:解析父组件传递的属性和非props属性
  3. ​执行setup()​​:组合式API入口,创建响应式数据,声明计算属性/方法,返回模板可用内容
  4. ​解决依赖关系​​:处理provide/inject依赖注入,建立父子关联

​关键点​​:此时组件数据和方法已初始化,但尚未生成DOM元素

二、挂载阶段(DOM插入页面)

1. 挂载前准备

  1. ​编译模板​​:将模板编译为渲染函数
  2. ​创建虚拟DOM​​:执行渲染函数生成虚拟节点树
  3. ​触发beforeMount​​:DOM创建前的最后准备阶段

2. 挂载过程

  1. ​生成真实DOM​​:将虚拟DOM转换为浏览器可识别的元素
  2. ​插入页面​​:将创建的元素添加到父容器中
  3. ​触发mounted​​:组件完成挂载,可安全操作DOM

​父子顺序​​:子组件先挂载,父组件后挂载

三、更新阶段(响应数据变化)

1. 触发时机

  • ​数据变更​​:组件内部响应式状态变化
  • ​Props更新​​:父组件传递新的props值
  • ​插槽变化​​:父组件中插槽内容更新
  • ​强制更新​​:调用 forceUpdate() 方法

2. 更新流程

  1. ​检测变化​​:Vue 的响应式系统检测到数据变化,标记组件为需要更新状态(dirty)
  2. ​触发beforeUpdate​​:组件状态已更新但DOM未渲染,可访问更新后的数据但DOM未改变
  3. ​重新渲染​​:执行渲染函数生成新虚拟DOM,Diff算法对比新旧虚拟DOM差异
  4. ​应用更新​​:仅修改实际变化的DOM元素,批量处理多个变更以提高性能
  5. ​触发updated​​:DOM更新完成,可安全操作更新后的DOM元素

​父子顺序​​:父组件先更新,子组件后更新

四、销毁阶段(移除组件)

1. 触发时机

  • ​条件渲染消失​​:v-if 变为 false 时销毁
  • ​动态组件切换​​:切换到其他组件时,销毁当前显示的组件
  • ​路由离开​​:路由切换到其他页面时,销毁当前路由对应的组件
  • ​父组件销毁​​:父组件被销毁,所有子组件也随之销毁
  • ​编程式卸载​​:调用 unmount() 方法销毁组件

2. 销毁流程

  1. ​触发beforeUnmount​​:组件即将被销毁但功能仍完整,最后一次访问组件状态和DOM
  2. ​停止响应式​​:移除所有响应式依赖追踪,停止 computed、watch 等响应式功能
  3. ​清理资源​​:自动移除模板中注册的事件监听,手动清除清除通过 addEventListener 添加的监听器、定时器、订阅等(需在钩子中实现)
  4. ​移除DOM​​:从DOM树中删除组件元素
  5. ​触发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 响应式系统的核心优化手段。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值