Vue的响应式一直是面试中的重点,大家也都知道Vue3的响应式是弃用了Vue2中使用的
Object.defineProperty
,而改用了ES6新增的Proxy
,我们来看一下Vue3的响应式是怎么做的
tip:本文需要Proxy、Reflect以及Vue3中的Effect等前置知识,如果不熟悉先去看下文档哦
一、 实现 reactive 函数
通过reactive
创建的对象是如何实现响应式的呢,核心就是利用 Proxy
拦截对象的读取和设置操作,并在这些操作中实现依赖收集和触发更新。
1.1 基本实现
我们先实现一个简单的 reactive
函数,让他能够支持在读取和写入的时候都被拦截到,方便我们做后续操作。
function reactive(target) {
const handler = {
get(target, key, receiver) {
console.log(`读取属性:${key}`);
return Reflect.get(target, key, receiver); // 可暂时理解为 return target[key]
},
set(target, key, value, receiver) {
console.log(`设置属性:${key} = ${value}`);
return Reflect.set(target, key, value, receiver); // 可暂时理解为 return target[key] = value)
},
};
return new Proxy(target, handler);
}
const obj = reactive({ name: '张三' });
obj.name; // 输出:读取属性:name
obj.name = '李四'; // 输出:设置属性:name = 李四
1.2 处理嵌套对象
如果对象的属性值也是一个对象,我们需要递归地将它转换为响应式对象。为此,可以在 get
拦截器中判断属性值是否为对象,如果是,则递归调用 reactive
。
function reactive(target) {
const handler = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
console.log(`读取属性:${key}`);
// 如果属性值是对象,则递归调用 reactive
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
console.log(`设置属性:${key} = ${value}`);
return Reflect.set(target, key, value, receiver);
},
};
return new Proxy(target, handler);
}
const obj = reactive({ name: '张三', info: { age: 25 } });
obj.info.age; // 输出:读取属性:info -> 读取属性:age
obj.info.age = 26; // 输出:读取属性:info -> 设置属性:age = 26
二、 依赖收集和触发更新
前面我们已经可以拦截到值的读取和写入了,那用到obj.name
的地方如何能在obj.name
写入新值的时候能及时更新呢??
那就要说到“依赖收集”和“依赖更新”了,也就是说在用到obj.name
的时候,我们对其进行统计,然后在obj.name
被赋新值后,把刚才名单上的用到obj.name
的某方法A带来的effect去执行
“好嘛,
fn1
和fn2
里都用到了obj.name
,把他俩带过来的回调(effect
)记到名单上,下次obj.name
写入的时候,把obj.name
上名单的effect
们挨个执行!”
tip:effect
指的是响应式系统中用于追踪依赖和触发更新的函数。
2.1 依赖收集
为了实现依赖收集,我们需要:
- 定义一个全局变量
activeEffect
,用于存储当前正在执行的副作用函数。 - 在
get
拦截器中,将activeEffect
收集到依赖集合中。
let activeEffect = null;
const targetMap = new WeakMap(); // 存储目标对象及其依赖,这里使用WeakMap而不是Map是为了利用其弱引用特性,避免内存泄漏,并更符合依赖收集的语义。
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
// 哦?target这个对象还没有创建过依赖?创建!
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let deps = depsMap.get(key);
// 啥?target.name这个name竟然没创建对应依赖?创建!
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
deps.add(activeEffect);
}
2.2 触发更新
在 set
拦截器中,我们需要从依赖集合中取出所有副作用函数并执行:
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (deps) {
deps.forEach(effect => effect());
}
}
2.3 整合到 reactive 中
将 track
和 trigger
整合到 reactive
中:
function reactive(target) {
const handler = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key); // 收集依赖
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
const success = Reflect.set(target, key, value, receiver);
// 判断是不是新值,不是新值就别白折腾了
if (success && oldValue !== value) {
trigger(target, key); // 触发更新
}
return success;
},
};
return new Proxy(target, handler);
}
三、完成
我们来测试一下:
function effect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}
const obj = reactive({ name: '张三', info: { age: 25 } });
effect(() => {
console.log(`名字:${obj.name}`);
});
effect(() => {
console.log(`年龄:${obj.info.age}`);
});
obj.name = '李四'; // 输出:名字:李四
obj.info.age = 26; // 输出:年龄:26
没有问题,完成~