在上一篇文章中,我们实现了一个简易版的 effect。但真实的 Vue3 中,effect 要复杂得多:它需要处理嵌套 effect、支持停止追踪、提供 runner 函数、实现调度器等。本文将一步步完善 effect 的实现,最终与Vue3源码对标。
前言:从极简到完善
在上一篇文章 副作用的概念与effect基础:Vue3响应式系统的核心 中,我们实现了一个简易版的 effect ,这个版本有哪些问题呢?
- 嵌套 effect 会破坏 activeEffect
- 无法停止 effect 的追踪
- 没有返回 runner 函数(可以手动执行)
- 不支持调度器(scheduler)
- 没有 effect 选项(lazy、onTrack等)
- 没有处理错误情况
本篇文章,将一步步解决这些问题。
全局 activeEffect 的设计挑战
为什么需要 activeEffect?
当执行 effect 时,我们怎么知道当前正在运行的 effect 到底是哪个呢?这就是 activeEffect 的作用,使用全局变量 activeEffect 可以记录当前正在执行的 effect 。
activeEffect 工作流程
- 使用 effect 注册副作用函数 fn1 ,执行effect(fn1)
- 并将副作用函数 fn1 赋值给activeEffect :
activeEffect = fn1 - fn1 执行,访问响应式数据
- track 函数被调用,将 activeEffect(fn1) 添加到依赖集合
- fn1 执行完毕,将 activeEffect 置空:
activeEffect = null
activeEffect 工作过程示例
javascript
function 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栈)
effect 是可以发生嵌套的,如以下示例:
javascript
effect(fn1() {
effect(fn2() {
effect(fn3(){
/* ... */
})
})
})
这段代码中,fn1 内部嵌套了 fn2;fn2 内部又嵌套了 fn3。当 fn1 执行时,会导致 fn2 的执行;当 fn2 执行时,又会导致 fn3 的执行。
为什么要处理 effect 嵌套问题?
以上述嵌套 effect 为例,如果我们用之前的 demonstrateActiveEffect() 函数处理,会发生什么问题呢?
此时,我们期望的结果是:fn1 执行过程中,fn2 开始执行,同时 fn3 也开始执行;fn1 和 fn2 应该被暂停。但实际上,当我们使用全局 activeEffect 来存储副作用函数时,它会被后面的副作用覆盖,即:fn2 会覆盖 fn1,fn3 会覆盖 fn2,导致最后的结果是只有 fn3 被收集了,无法再收集 fn1 和 fn2 。
为了解决这个问题,我们就需要一个副作用栈 effectStack ,在副作用函数执行时,将当前副作用函数压入栈底,待副作用函数执行完成后,再将其弹出,并始终让 activeEffect 指向栈顶的副作用函数。
javascript
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;
}
}
使用 effect 栈解决嵌套问题
javascript
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结束');
});
}
什么时候会出现嵌套的 effect 呢?
在 Vue 中,当我们使用了嵌套组件时,其实就发生了 effect 嵌套:
javascript
<!-- Foo.vue -->
<template>
<div>
<Bar />
</div>
</template>
<script setup lang="ts">
import Bar from './bar.vue'
</script>
上述代码相当于:
javascript
effect(() => {
Foo.render();
effect(() => {
Bar.render();
});
})
effect 返回 runner 函数
runner函数的作用
effect 默认立即执行一次,但有时我们希望手动控制执行时机,因此我们就希望 effect 能返回一个函数,我们可以通过这个函数手动触发 effect 重新执行。
使用场景
- 懒执行的effect(lazy: true)
- 需要手动触发的更新
- 可以随时停止的effect
实现带 runner 的 effect
javascript
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;
}
runner的返回值
方案1:返回fn的执行结果
javascript
function effect1(fn) {
return fn();
}
方案2:返回runner函数
javascript
function effect2(fn) {
const runner = () => fn();
runner();
return runner;
}
方案3:返回effect实例(Vue3的做法)
javascript
function effect3(fn) {
const _effect = new ReactiveEffect(fn);
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
runner();
return runner;
}
完整的effect实现
javascript
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 的执行调度、懒执行和停止跟踪等内容。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!