🔥 深入浅出 Vue3 响应式原理:从 Proxy 到手写核心代码
前言
只要你是 Vue 开发者,面试时一定逃不开这个问题:“请说一下 Vue 的响应式原理”。
很多同学能背出“Vue2 用 Object.defineProperty,Vue3 用 Proxy”,但如果面试官继续深挖:“为什么用 Proxy?依赖是怎么收集的?Reflect 有什么用?”可能就会一脸懵。
今天,我们就用最通俗易懂的语言,由简入深地扒开 Vue3 响应式系统的外衣,最后带你手写一个 Mini 版的 Vue3 响应式核心!🚀
1. 为什么要抛弃 Object.defineProperty?
在讲 Vue3 之前,我们先鞭尸一下 Vue2。Vue2 使用 Object.defineProperty 来劫持对象的 getter 和 setter。但它有几个致命的缺点:
- 无法监听对象属性的新增和删除(所以才有了
$set和$delete)。 - 无法原生监听数组的索引和长度变化(Vue2 内部 hack 了数组的方法)。
- 必须深层遍历:如果对象层级很深,Vue2 在初始化时就会递归遍历所有属性,非常消耗性能。
为了解决这些痛点,Vue3 拥抱了 ES6 的新特性:Proxy。
2. 核心基石:Proxy 与 Reflect
什么是 Proxy?
Proxy 顾名思义就是“代理”。你可以把它理解为对象外层的一层**“安检门”**。无论你是想读取对象的属性,还是修改对象的属性,都必须经过这扇门。
const target = { name: '尤雨溪', age: 18 };
const proxy = new Proxy(target, {
get(target, key) {
console.log(`👀 拦截到了读取:${key}`);
return target[key];
},
set(target, key, value) {
console.log(`✍️ 拦截到了设置:${key} = ${value}`);
target[key] = value;
return true;
}
});
proxy.name; // 控制台输出:👀 拦截到了读取:name
proxy.age = 20; // 控制台输出:✍️ 拦截到了设置:age = 20
Proxy 的优势:它代理的是整个对象,而不是对象的某个属性。所以无论是新增属性还是删除属性,甚至数组的变化,它都能拦截到!而且它是惰性的,只有你访问到深层属性时,才会去代理深层属性。
为什么还需要 Reflect?
在 Vue3 的源码中,Proxy 永远是和 Reflect 结对出现的。为什么不直接 return target[key] 呢?
核心原因是为了保证 this 的指向正确。
假设我们有这样一个对象:
const obj = {
firstName: '尤',
lastName: '雨溪',
get fullName() {
return this.firstName + this.lastName;
}
};
如果我们在 Proxy 的 get 中直接 return target[key],当访问 proxy.fullName 时,fullName 内部的 this 会指向原始对象 obj,而不是代理对象 proxy。这会导致 firstName 和 lastName 的读取无法被拦截!
而 Reflect.get(target, key, receiver) 中的 receiver 就是代理对象本身,它能把 this 纠正为代理对象。
3. 响应式系统的三大件:effect、track、trigger
有了 Proxy 拦截数据还不够,我们还需要知道:数据变化时,到底该通知谁去更新?
Vue3 响应式系统有三个核心概念:
effect(副作用函数):你可以把它理解为“谁在使用数据”。比如组件的渲染函数、watch回调等。track(依赖收集):在 Proxy 的get中触发。把当前的effect记录下来。trigger(派发更新):在 Proxy 的set中触发。数据变了,把之前记录的effect拿出来执行一遍。
依赖是怎么存储的?(重点!)
Vue3 设计了一个非常巧妙的数据结构来存储依赖,它是一个三层嵌套的结构:WeakMap -> Map -> Set。
WeakMap:它的 key 是目标对象(target),value 是一个Map。(使用 WeakMap 是为了防止内存泄漏,对象销毁时依赖也会自动回收)。Map:它的 key 是对象的属性名(key),value 是一个Set。Set:里面存的就是一个个的effect函数(因为同一个属性可能被多个地方使用,Set 可以去重)。
结构图如下:
WeakMap {
{ name: 'Vue3', age: 3 } : Map {
'name' : Set [ effect1, effect2 ],
'age' : Set [ effect3 ]
}
}
4. 手写一个 Mini 版响应式系统
纸上得来终觉浅,绝知此事要躬行。我们把上面的理论转化为代码!
// 1. 存储依赖的全局结构
const targetMap = new WeakMap();
// 2. 记录当前正在执行的 effect
let activeEffect = null;
// 3. effect 函数:包装用户的回调
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn; // 执行前,把自己暴露到全局
fn(); // 执行用户函数,这会触发 Proxy 的 get
activeEffect = null; // 执行完,清理掉
};
effectFn(); // 立即执行一次,完成初始的依赖收集
}
// 4. track:依赖收集
function track(target, key) {
if (!activeEffect) return; // 如果没有 activeEffect,说明不是在 effect 中读取的,不管它
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(activeEffect); // 把当前的 effect 存进去!
}
// 5. trigger:派发更新
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effectFn => effectFn()); // 数据变了,把存起来的 effect 全都执行一遍!
}
}
// 6. reactive:创建响应式对象
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 收集依赖
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
// 派发更新
trigger(target, key);
return result;
}
});
}
测试一下我们的代码!
const state = reactive({ count: 0, name: 'Vue' });
// 模拟组件渲染
effect(() => {
console.log(`🔄 视图更新啦!当前 count 为:${state.count}`);
});
// 初始打印:🔄 视图更新啦!当前 count 为:0
console.log('--- 修改数据 ---');
state.count++;
// 打印:🔄 视图更新啦!当前 count 为:1
state.count++;
// 打印:🔄 视图更新啦!当前 count 为:2
// 修改未在 effect 中使用的属性,不会触发更新
state.name = 'Vue3';
总结
Vue3 的响应式原理其实就是一场**“发布-订阅”**的精妙演出:
reactive利用Proxy设立了安检门,结合Reflect保证了this的绝对正确。effect是舞台上的演员,它在登台(执行)前会大喊一声“我现在是activeEffect!”。track是安检门的记录员(get拦截),它看到activeEffect访问了某个属性,就把他记在小本本(WeakMap -> Map -> Set)上。trigger是安检门的广播员(set拦截),一旦有人修改了属性,他就翻开小本本,把记录在案的演员(effect)全都叫出来重新表演一次。
希望这篇文章能帮你彻底搞懂 Vue3 的响应式原理!如果觉得有帮助,别忘了点赞收藏哦~ 👍✨