二次元贴图
50.51M · 2026-02-26
在上一篇文章 副作用的概念与effect基础:Vue3响应式系统的核心 中,我们实现了一个简易版的 effect ,这个版本有哪些问题呢?
本篇文章,将一步步解决这些问题。
当执行 effect 时,我们怎么知道当前正在运行的 effect 到底是哪个呢?这就是 activeEffect 的作用,使用全局变量 activeEffect 可以记录当前正在执行的 effect 。
activeEffect = fn1activeEffect = nullfunction demonstrateActiveEffect() {
let activeEffect = null;
function track() {
console.log(` [track] 收集依赖,当前effect: ${activeEffect?.name || 'null'}`);
}
function run(effect) {
console.log(` [run] 开始执行 ${effect.name}`);
activeEffect = effect;
effect.fn();
activeEffect = null;
console.log(` [run] 结束执行 ${effect.name}`);
}
const effect1 = {
name: 'effect1',
fn: () => {
console.log(' effect1执行中');
track();
}
};
const effect2 = {
name: 'effect2',
fn: () => {
console.log(' effect2执行中');
track();
}
};
console.log('1. 执行effect1:');
run(effect1);
console.log('n2. 执行effect2:');
run(effect2);
}
effect 是可以发生嵌套的,如以下示例:
effect(fn1() {
effect(fn2() {
effect(fn3(){
/* ... */
})
})
})
这段代码中,fn1 内部嵌套了 fn2;fn2 内部又嵌套了 fn3。当 fn1 执行时,会导致 fn2 的执行;当 fn2 执行时,又会导致 fn3 的执行。
以上述嵌套 effect 为例,如果我们用之前的 demonstrateActiveEffect() 函数处理,会发生什么问题呢?
此时,我们期望的结果是:fn1 执行过程中,fn2 开始执行,同时 fn3 也开始执行;fn1 和 fn2 应该被暂停。但实际上,当我们使用全局 activeEffect 来存储副作用函数时,它会被后面的副作用覆盖,即:fn2 会覆盖 fn1,fn3 会覆盖 fn2,导致最后的结果是只有 fn3 被收集了,无法再收集 fn1 和 fn2 。
为了解决这个问题,我们就需要一个副作用栈 effectStack ,在副作用函数执行时,将当前副作用函数压入栈底,待副作用函数执行完成后,再将其弹出,并始终让 activeEffect 指向栈顶的副作用函数。
class EffectStack {
constructor() {
this.stack = [];
this.current = null;
}
// 入栈
push(effect) {
console.log(` [栈] 入栈: ${effect.name || 'anonymous'}`);
this.stack.push(effect);
this.current = effect;
}
// 出栈
pop() {
const popped = this.stack.pop();
console.log(` [栈] 出栈: ${popped?.name || 'anonymous'}`);
this.current = this.stack[this.stack.length - 1] || null;
return popped;
}
// 获取当前effect
getCurrent() {
return this.current;
}
}
function demonstrateEffectStack() {
const effectStack = new EffectStack();
function track() {
const current = effectStack.getCurrent();
console.log(` [track] 当前effect: ${current?.name || 'null'}`);
}
function effect(fn) {
const effectFn = () => {
effectStack.push(effectFn);
fn();
effectStack.pop();
};
effectFn.name = fn.name;
effectFn();
return effectFn;
}
console.log('使用effect栈后:');
effect(function effect1() {
console.log(' effect1开始');
track(); // 收集effect1
effect(function effect2() {
console.log(' effect2开始');
track(); // 收集effect2
console.log(' effect2结束');
});
track(); // 现在能正确收集effect1了!
console.log(' effect1结束');
});
}
在 Vue 中,当我们使用了嵌套组件时,其实就发生了 effect 嵌套:
<!-- Foo.vue -->
<template>
<div>
<Bar />
</div>
</template>
<script setup lang="ts">
import Bar from './bar.vue'
</script>
上述代码相当于:
effect(() => {
Foo.render();
effect(() => {
Bar.render();
});
})
effect 默认立即执行一次,但有时我们希望手动控制执行时机,因此我们就希望 effect 能返回一个函数,我们可以通过这个函数手动触发 effect 重新执行。
function effectWithRunner(fn, options = {}) {
const _effect = new ReactiveEffect(fn);
// runner函数
const runner = () => {
return _effect.run();
};
// 保存effect实例到runner上,方便后续操作
runner.effect = _effect;
// 如果不是懒执行,立即运行
if (!options.lazy) {
runner();
}
return runner;
}
function effect1(fn) {
return fn();
}
function effect2(fn) {
const runner = () => fn();
runner();
return runner;
}
function effect3(fn) {
const _effect = new ReactiveEffect(fn);
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
runner();
return runner;
}
console.log('n=== 完整版effect实现 ===n');
// 依赖存储
const targetMap = new WeakMap();
// effect栈
const effectStack = [];
// 当前激活的effect
function getCurrentEffect() {
return effectStack[effectStack.length - 1];
}
// ReactiveEffect类
class ReactiveEffect {
constructor(fn, scheduler = null) {
this.fn = fn;
this.scheduler = scheduler;
this.deps = [];
this.active = true; // 是否激活
this.name = fn.name || 'anonymous';
}
run() {
if (!this.active) {
return this.fn();
}
try {
// 入栈
effectStack.push(this);
cleanupEffect(this); // 清除旧的依赖
console.log(` [run] 开始执行 ${this.name}`);
// 执行fn,期间会触发track
return this.fn();
} finally {
// 出栈
effectStack.pop();
console.log(` [run] 结束执行 ${this.name}`);
}
}
stop() {
if (this.active) {
cleanupEffect(this);
this.active = false;
}
}
}
// 清除effect的所有依赖
function cleanupEffect(effect) {
const { deps } = effect;
if (deps.length) {
console.log(` [cleanup] 清除 ${effect.name} 的 ${deps.length} 个依赖`);
deps.forEach(dep => dep.delete(effect));
deps.length = 0;
}
}
// 依赖收集
function track(target, key) {
const activeEffect = getCurrentEffect();
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
console.log(` [track] ${activeEffect.name} 依赖了 ${key}`);
}
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (!dep) return;
console.log(` [trigger] ${key} 变化,触发 ${dep.size} 个effect`);
// 复制一份,防止在遍历过程中修改Set
const effects = new Set(dep);
effects.forEach(effect => {
if (effect !== getCurrentEffect()) { // 避免无限循环
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
});
}
// 主effect函数
function effect(fn, options = {}) {
const { lazy = false, scheduler = null } = options;
const _effect = new ReactiveEffect(fn, scheduler);
// 创建runner函数
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
// 立即执行(除非是懒执行)
if (!lazy) {
runner();
}
return runner;
}
本篇文章简单介绍了 activeEffect 的设计与挑战,以及嵌套 effect 的处理。下一篇文章中,我们将介绍 effect 的执行调度、懒执行和停止跟踪等内容。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!