返回首页

Vue3响应式原理

布莱克2026-05-14 17:27已编辑
Tip:文章封面与内容无关,作者旅游时拍摄,因为没什么值得把四季都错过!

Vue3的响应系统的实现

什么是响应式?

当数据发生变化时,系统能够自动检测到,并触发依赖这些数据的相关任务(如更新 DOM、执行 watch 回调等)

说明一下几个核心的概念:

副作用函数:

副作用函数指的是会产生“副作用”的函数,即函数执行时会读取或修改外部状态,只要函数执行会对外部产生影响(读取数据、修改数据、DOM操作等),就是副作用函数

响应式数据:

能够自动追踪依赖并在数据变化时自动触发更新的数据

依赖追踪 - 知道哪些代码依赖自己

变化侦测 - 知道自己何时发生了变化

更新触发 - 变化时自动通知所有依赖方更新


响应式数据的实现🤔

Vue2的实现:Object.defineProperty

Vue 2 在初始化数据时,会递归遍历 data 对象的所有属性,使用 Object.defineProperty 劫持数据的 getter/setter。

在 getter 中收集依赖(Watcher),在 setter 中触发更新

配合递归遍历实现深度响应,对象层级越深、属性越多,初始化越慢,内存占用大

  • Getter:触发 dep.depend(),收集当前正在执行的 Watcher。
  • Setter:触发 dep.notify(),通知 Watcher 去排队更新。
  • 缺陷: 无法监听对象属性的添加或删除,无法原生监听数组索引修改。这也就是为什么在 Vue 2 中需要使用 this.$set

不支持数组,vue重写了数组方法,无法监听新增/删除属性,需要使用Vue.set() Vue.delete()


Vue3响应式的实现:

使用代理对象proxy实现,不再拦截具体的属性,而是拦截整个对象,Proxy 代理 + Reflect + 副作用收集与触发机制

Proxy 是 ES6 语法,允许创建一个对象的代理,从而拦截和自定义该对象的基本操作

proxy构造函数接收两个参数,第一个是被代理的对象,第二个对象包含get函数用来拦截读取操作,set函数用来拦截设置操作


核心数据结构

创建一个用于存储副作用函数的“桶” WeakMap

读取操作发生时,将副作用函数收集到”桶“中

设置操作发生时,从“桶”中取出副作用函数并执行

关于“桶”:

WeakMap -> Map -> Set 结构

底层通过 WeakMap 以对象实例为索引,中层通过 Map 以属性标识符为索引,顶层则利用 Set 维护一个 副作用函数队列。这种设计不仅实现了内存敏感的自动释放,更通过 Key-to-Effects 的映射实现了组件更新的最小化调度

为什么第一层是 WeakMap

  • 内存优化(架构关键点):WeakMap 的键是弱引用。这意味着如果你的原始对象(target)在业务代码中不再被引用了,垃圾回收机制(GC)会自动把这个对象及其对应的“依赖桶”一起回收。
  • 防止内存泄漏:如果是普通的 Map,即使对象销毁了,由于“桶”还拽着它,对象永远无法释放,导致内存溢出。

为什么第二层是 Map

  • 属性隔离:一个对象可能有几百个属性,只有当 obj.a 变化时,才应该触发依赖 a 的函数。Map 建立了 key 到 effects 的一对一映射。

为什么第三层是 Set

  • 去重:一个副作用函数可能多次读取同一个属性。使用 Set 可以确保同一个 effect 在同一属性下只被存储一次,避免重复触发。

在 get 时通过 track 将 activeEffect 注入桶中,在 set 时通过 trigger 配合 Scheduler(调度器) 实现异步批处理更新

var data = {name: '布莱克', age: 18 }
const buckt = new WeakMap()
const obj = new Proxy(data, {
      get(target, key){
            //将副作用函数 activeEffect 添加到存储副作用函数的桶中
            track(target, key)
            return target[key]
      },
      set(target, key, newVal){
            target[key] = newVal
            //把副作用函数从桶中取出并执行
            trigger(target, key)
      }
})
//用一个全局变量存储被注册的副作用函数
let activeEffect
//在get拦截函数内调用track函数追踪变化
function track(target, key){
       if(!activeEffect) return
       let depsMap = buckt.get(target)
       if(!depsMap){
            buckt.set(target, (depsMap = new Map()))
       }
       let deps = depsMap.get(key)
       if(!deps){
            depsMap.push(key, (deps = new Set()))
       }
       deps.add(activeEffect)
}
//在set拦截函数内部调用trigger函数触发变化
function trigger(target, key){
       const depsMap = buckt.get(target)
       if(!depsMap) return
       const deps = depsMap.get(key)
       deps && deps.forEach(fn => fn())
}

调度器:

调度器的本质是一个 队列管理系统。它利用了 JavaScript 的 微任务(Microtask) 机制,将同步的数据变更“攒”起来,延迟到同步代码执行完后统一处理

  • 触发更新:数据变化,trigger 被触发。
  • 入队(Enqueue):调度器并不立即执行更新,而是将当前的副作用函数(Job)丢进一个全局队列 queue。(会进行去重)
  • 开启刷新(Flush):如果当前没有正在刷新的任务,调度器会自动通过 Promise.resolve().then(flushJobs) 开启一个微任务。
  • 异步执行:主线程同步代码跑完后,JS 引擎自动执行微任务队列中的 flushJobs,此时才真正开始遍历 queue 执行更新,在微任务真正执行(flushJobs)时,它会先对队列进行排序,确保父组件(先创建)比子组件(后创建)先更新


在用户层面使用的是ref,reactive等API,他们之间如何联系呢?

第一层:开发者接口(API Layer)
├── ref()      - 通用响应式包装器
├── reactive() - 对象响应式代理
├── computed() - 计算响应式
└── readonly() - 只读包装

第二层:核心转换层(Core Transformation Layer)
├── toReactive()   - 值到响应式转换
├── toRaw()        - 获取原始对象
├── isProxy()      - 代理检测
└── markRaw()      - 标记为非响应式

第三层:底层实现层(Implementation Layer)
├── Proxy/Reflect - ES6 代理机制
├── WeakMap       - 依赖存储
├── Set/Map       - 依赖收集
└── Effect System - 副作用管理

基本类型无法被 Proxy 代理,所以用 ref 包装成 { value: xxx } 对象,再劫持 value 的 get/set

ref设计思想是:将任何数据类型包装成一个响应式对象,通过 .value 访问和修改

ref 包装对象时,是深度响应式,会在初始化时递归地把所有嵌套对象都转成响应式,而不是等到访问时才转换


reactive 用于创建深度响应式对象的 API,核心就是 Proxy + 懒代理 + 缓存机制

  1. Proxy 拦截对象的 get/set/deleteProperty 等操作,实现完整的响应式
  2. 懒代理:只有访问到嵌套对象时才递归代理,将其转化为响应式,性能比 Vue 2 的初始化全量递归好很多
  3. WeakMap 缓存:避免同一个原始对象被重复代理,且不会阻止垃圾回收

只能代理对象、解构会丢失响应式,所以基本类型要用 ref,解构时配合 toRefs


computed 计算属性

  • 计算属性只在依赖变化且被读取时才重新计算
  • 如果 computed 没有被模板/其他 effect 使用,即使依赖变了也不会计算

computed 用于派生数据,必须 return 值,有缓存机制。只有依赖变化且被读取时才重新计算,适合做数据格式化、列表过滤


watch 监听器

用于执行副作用,不需要返回值,没有缓存。每次依赖变化都会执行回调,适合发请求、操作 DOM、存缓存

维度computedwatch
本质派生数据副作用
是否返回值✅ 必须返回❌ 不返回
缓存机制✅ 有缓存❌ 无缓存
执行时机懒执行(被读取时)立即/延迟执行
异步支持❌ 不支持✅ 支持
适用场景数据格式化、过滤、计算API 请求、DOM 操作、路由跳转

watch默认创建时不跑,等数据变化了才跑

执行时机watch(默认)watch(immediate: true)watchEffect
初始化时❌ 不执行✅ 立即执行一次✅ 立即执行一次
数据变化时✅ 执行✅ 执行✅ 执行


watch和watchEffect

特性watchwatchEffect
懒执行默认懒执行(数据变化后才执行)立即执行一次
依赖收集明确指定监听的源自动收集函数内使用的响应式数据
访问旧值可以获取旧值和新值只能获取新值
监听深度默认浅层,可通过配置开启深层自动深层追踪所有依赖
停止时机组件卸载时自动停止组件卸载时自动停止
flush 时机默认 pre(组件更新前)默认 pre(组件更新前)
  • watch 需要手动声明依赖,适合精准监听特定数据
  • watchEffect 自动收集依赖,适合多个数据联动的场景
import { ref, watch, watchEffect } from 'vue';

const count = ref(0);
const name = ref('John');
const user = ref({ age: 18 });

// ========== watch ==========
// 手动指定依赖
watch(count, (newVal, oldVal) => {
  console.log(`count 从 ${oldVal} 变成 ${newVal}`);
}, { immediate: true }); // 需要配置才立即执行

// ========== watchEffect ==========
// 自动收集依赖,立即执行一次
watchEffect(() => {
  // 里面用到了谁,就自动监听谁
  console.log(`count 是 ${count.value},name 是 ${name.value}`);
  // 会同时监听 count 和 name
});



assistant