前言
(⚠️长篇代码警告⚠️)
密码的,好难😇,看了好几遍视频,还让豆包和kimi给我解释代码,果然是我太菜了吗...
这节讲的是如何实现vue框架中使用的effect,实现将响应式数据和依赖双向关联,创建响应式的副作用函数,在响应式数据变化时及时重新调用副作用函数。
effect似乎在平时开发中不常用,只有在构建框架时可能用到,所以并没有完全吃透,尚且有些囫囵吞枣,大概了解了一下实现的思路...🤫
感觉整体上比较巧妙的点在于数据结构的组织以及在reactiveEffect类型中添加parent字段。
-
weakMap={对象:Map:{属性:set}},这部分很值得思考:首先weakMap支持对象作为键值,并且有更好的垃圾处理机制;其次,使用Map作为对象的映射,一个对象正好对应多个属性key,也符合它采用string类型作为键值的规定;最后,使用set来保存某个键值key对应的多个副作用函数,而且使用set最讨巧的点在于它可以自动去重。(虽然在代码逻辑里已经手动去重😗) -
听闻
vue之前版本的代码不是通过为reactiveEffect添加parent字段来实现嵌套effect的,而是通过数组??然后改成了现在更加简单省内存的版本,使用新字段parent来记录父级effect,这部分其实也挺精妙,一个简单的改动却很大的提升了性能。👍
代码
effect.ts
export let activeEffect = undefined; // 存储当前的effect
class ReactiveEffect {
public deps = [];
// 反向收集effect关联了哪些属性
public parent = null;
public active = true; //实例上新增active属性 默认是激活的
constructor(public fn) {} // ts的语法,用户传递的参数也会挂到this上
run() {
// 此处表示非激活的情况,只执行函数,不进行依赖收集
if (!this.active) {
this.fn();
}
// 此处进行依赖收集,核心是将effect和当前的渲染关联
try {
this.parent = activeEffect;
activeEffect = this;
return this.fn();
// 稍后调用取值操作时,可以获取到全局的activeEffect
} finally {
// 即使发生了错误,也重置activeEffect
this.parent = null;
activeEffect = this.parent;
}
}
}
export function effect(fn) {
// 这里fn可以根据状态变化,重新执行,effect可以嵌套写
const _effect = new ReactiveEffect(fn); // 创建响应式的effect
_effect.run(); // 默认先执行一次
}
// 一个effect对应多个属性,一个属性对应多个effect
// 多对多的关系,需要一个中间结构,来存储这种对应关系
// weakMap = {object:map{name:'effect'}}
const targetMap = new WeakMap();
export function track(target, type, key) {
if (!activeEffect) return;
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()));
}
let shouldTrack = !dep.has(activeEffect);
// 如果dep中有没有activeEffect,那么就需要收集了,实现去重
if (shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep); // 让effect记录dep,稍后清理的时候会用到
}
// 对象的某个属性-> 多个effect
// weakMap={对象:map:{属性:set}},set实现去重
}
export function trigger(target, type, key, value, oldValue) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 触发的值不在模板中使用,不需要触发
const effects = depsMap.get(key);
// effects是一个set类型数据,包含所有与当前属性key相关联的effect
// 找到对应的effect
effects && effects.forEach((effect) => {
if (effect !== activeEffect) {
// 在执行effect的时候,又要执行自身,那就需要屏蔽掉,不要无限调用
effect.run();
}
});
}
baseHandler.ts
import { isObject } from '@vue/shared';
import { reactive } from './reactive';
import { activeEffect, track, trigger } from './effect';
export const enum ReactiveFlags { //响应式标识
// 实现同一个对象代理多次,返回同一个代理对象
// 代理对象的代理对象还是之前的代理对象
IS_REACTIVE = '__v_isReactive',
}
export const mutableHandlers = {
get(target, key, receiver) {
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
track(target, 'get', key);
// 去代理对象上取值 使用get
// return target[key]; 这种方式this指向有问题
console.log(key);
// 可以监控到用户取值
const value = Reflect.get(target, key, receiver);
// 如果获取的值是对象,递归调用 reactive 函数将其转换为响应式对象
if (isObject(value)) {
return reactive(value);
}
return value;
// Proxy要配合Reflect使用,保证this指向正确
// Reflect的recerver参数使this指向代理对象
},
set(target, key, value, receiver) {
// 去代理上设置值 使用set
let oldValue = target[key];
let result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
// 触发effect,更新
trigger(target, 'set', key, value, oldValue);
}
// 可以监控到用户设置值
return Reflect.set(target, key, value, receiver);
}
};
reactive.ts
import { isObject } from '@vue/shared';
import { mutableHandlers, ReactiveFlags } from './baseHandler';
// 1.将数据转换成响应式数据,只能做对象类型数据的代理
const reactiveMap = new WeakMap(); //key只能是对象类型
export function reactive(target) {
if (!isObject(target)) {
return target;
}
if (target[ReactiveFlags.IS_REACTIVE]) {
//如果target是代理对象,那么它一定会在这一步中进入get
return target; // 判定为代理对象,直接返回,对应多重代理问题
}
// 并没有重新定义属性,只是代理,在取值的时候会调用get,赋值时会调用set
let existingProxy = reactiveMap.get(target);
if (existingProxy) {
return existingProxy;
}
// 第一次普通对象代理,会通过new Proxy代理一次
// 下一次传入proxy对象,为了检测是否代理过,可以查看是否有get方法,有的话说明被proxy代理
const proxy = new Proxy(target, mutableHandlers);
reactiveMap.set(target, proxy);
return proxy;
}
代码解释
在 Vue 的响应式系统中, effect 是一个核心概念,它的主要作用是创建一个响应式的副作用函数,当响应式数据发生变化时,这个副作用函数会自动重新执行。
effect 函数用于创建一个响应式的副作用函数。当副作用函数中访问了响应式对象的属性时,Vue 的响应式系统会自动跟踪这些依赖关系。一旦这些响应式属性的值发生变化,副作用函数会被重新执行,从而实现数据的自动更新。
effect函数实现
activeEffect:它是一个全局变量,用于存储当前正在执行的effect。在依赖收集过程中,通过这个变量可以知道当前是哪个 effect 在访问响应式数据。ReactiveEffect类 :用于封装副作用函数fn,并提供run方法来执行副作用函数。在run方法中,会将当前的effect赋值给activeEffect,以便在访问响应式数据时进行依赖收集。effect函数 :创建一个ReactiveEffect实例,并调用其run方法,默认先执行一次副作用函数。
依赖收集
targetMap:它是一个WeakMap,用于存储对象和其对应的依赖关系。每个对象对应一个Map,这个Map存储了对象的属性和对应的effect集合。track函数 :当访问响应式对象的属性时,会调用track函数进行依赖收集。它会检查当前是否有activeEffect,如果有,则将该effect添加到对应的属性的依赖集合中。
触发更新
trigger函数 :当修改响应式对象的属性时,会调用trigger函数触发更新。它会从targetMap中找到对应的依赖集合,并遍历这些effect,调用它们的run方法重新执行副作用函数。
响应式对象的代理
reactive函数 :用于创建响应式对象的代理。它使用Proxy对象对目标对象进行代理,并使用mutableHandlers作为代理的处理程序。mutableHandlers:定义了get和set拦截器。在get拦截器中,会调用track函数进行依赖收集;在set拦截器中,会调用trigger函数触发更新。