什么是响应式?
当数据发生变化时,系统能够自动检测到,并触发依赖这些数据的相关任务(如更新 DOM、执行 watch 回调等)
说明一下几个核心的概念:
副作用函数指的是会产生“副作用”的函数,即函数执行时会读取或修改外部状态,只要函数执行会对外部产生影响(读取数据、修改数据、DOM操作等),就是副作用函数
能够自动追踪依赖并在数据变化时自动触发更新的数据
依赖追踪 - 知道哪些代码依赖自己
变化侦测 - 知道自己何时发生了变化
更新触发 - 变化时自动通知所有依赖方更新
响应式数据的实现🤔
Vue2的实现:Object.defineProperty
Vue 2 在初始化数据时,会递归遍历 data 对象的所有属性,使用 Object.defineProperty 劫持数据的 getter/setter。
在 getter 中收集依赖(Watcher),在 setter 中触发更新
配合递归遍历实现深度响应,对象层级越深、属性越多,初始化越慢,内存占用大
不支持数组,vue重写了数组方法,无法监听新增/删除属性,需要使用Vue.set() Vue.delete()
使用代理对象proxy实现,不再拦截具体的属性,而是拦截整个对象,Proxy 代理 + Reflect + 副作用收集与触发机制
Proxy 是 ES6 语法,允许创建一个对象的代理,从而拦截和自定义该对象的基本操作
proxy构造函数接收两个参数,第一个是被代理的对象,第二个对象包含get函数用来拦截读取操作,set函数用来拦截设置操作
核心数据结构
创建一个用于存储副作用函数的“桶” WeakMap
读取操作发生时,将副作用函数收集到”桶“中
设置操作发生时,从“桶”中取出副作用函数并执行
关于“桶”:
WeakMap -> Map -> Set 结构
底层通过 WeakMap 以对象实例为索引,中层通过 Map 以属性标识符为索引,顶层则利用 Set 维护一个 副作用函数队列。这种设计不仅实现了内存敏感的自动释放,更通过 Key-to-Effects 的映射实现了组件更新的最小化调度
WeakMap?Map?Set?在 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) 机制,将同步的数据变更“攒”起来,延迟到同步代码执行完后统一处理
在用户层面使用的是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 + 懒代理 + 缓存机制
只能代理对象、解构会丢失响应式,所以基本类型要用 ref,解构时配合 toRefs
computed 用于派生数据,必须 return 值,有缓存机制。只有依赖变化且被读取时才重新计算,适合做数据格式化、列表过滤
用于执行副作用,不需要返回值,没有缓存。每次依赖变化都会执行回调,适合发请求、操作 DOM、存缓存
| 维度 | computed | watch |
|---|---|---|
| 本质 | 派生数据 | 副作用 |
| 是否返回值 | ✅ 必须返回 | ❌ 不返回 |
| 缓存机制 | ✅ 有缓存 | ❌ 无缓存 |
| 执行时机 | 懒执行(被读取时) | 立即/延迟执行 |
| 异步支持 | ❌ 不支持 | ✅ 支持 |
| 适用场景 | 数据格式化、过滤、计算 | API 请求、DOM 操作、路由跳转 |
watch默认创建时不跑,等数据变化了才跑
| 执行时机 | watch(默认) | watch(immediate: true) | watchEffect |
|---|---|---|---|
| 初始化时 | ❌ 不执行 | ✅ 立即执行一次 | ✅ 立即执行一次 |
| 数据变化时 | ✅ 执行 | ✅ 执行 | ✅ 执行 |
| 特性 | watch | watchEffect |
|---|---|---|
| 懒执行 | 默认懒执行(数据变化后才执行) | 立即执行一次 |
| 依赖收集 | 明确指定监听的源 | 自动收集函数内使用的响应式数据 |
| 访问旧值 | 可以获取旧值和新值 | 只能获取新值 |
| 监听深度 | 默认浅层,可通过配置开启深层 | 自动深层追踪所有依赖 |
| 停止时机 | 组件卸载时自动停止 | 组件卸载时自动停止 |
| flush 时机 | 默认 pre(组件更新前) | 默认 pre(组件更新前) |
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
});