effect的调度与清理:深入Vue3响应式系统的进阶特性

0 阅读5分钟

上一篇文章中,我们实现了基本的 effect,支持嵌套和 runner。但真实的响应式系统还需要处理更复杂的情况:分支切换导致的无效依赖、调度器控制执行时机、停止 effect 回收资源等,本文将深入这些高级特性。

前言:从分支切换的问题说起

const state = reactive({ ok: true, text: 'hello' });

effect(() => {
    console.log(state.ok ? state.text : 'not ok');
});

上述代码中,当 state.oktrue 时,effect 依赖了 oktext ; 当 state.okfalse 时,effect 只依赖了 ok ,此时 text 的依赖应该被清理掉!

这就是分支切换需要解决的问题。

分支切换与cleanup

什么是分支切换?

前言 中的代码为例,effect 内部存在一个三元表达式:state.ok ? state.text : 'not ok' ,根据字段 state.ok 值的不同,代码执行的分支会随之变化,执行不同的代码分支,这就是所谓的 分支切换

分支切换带来的问题

分支切换会产生遗留的副作用函数,上述例子中,当字段 state.ok 值为 false 时,此时 state.text 并不会被读取使用,但仍然会被依赖收集,这就产生了遗留的副作用,这显然是不合理的。

cleanup的作用

为了解决分支切换的问题,我们需要使用 cleanup 函数进行处理。该函数接收副作用函数作为参数,遍历副作用函数中的依赖集合,然后将该副作用从依赖集合中移除。这样,就可以避免副作用函数产生遗留问题了:

function cleanup(effect) {
    const { deps } = effect;
    if (deps.length) {
        console.log(`   [cleanup] 清除 ${effect.name}${deps.length} 个旧依赖`);
        deps.forEach(dep => dep.delete(effect));
        deps.length = 0;
    }
}

cleanup 的执行时机

每次 effect 执行之前,都会先调用 cleanup 函数,清理所有旧的依赖关系。以此确保依赖关系始终是最新的,避免无效更新:

run() {
    // 先清理所有旧依赖
    cleanup(this);
    
    // 设置为当前effect
    activeEffect = this;
    effectStack.push(this);
    
    console.log(`   [run] ${this.name} 执行`);
    
    try {
        // 执行fn,重新收集依赖
        this.fn();
    } finally {
        // 恢复activeEffect
        effectStack.pop();
        activeEffect = effectStack[effectStack.length - 1];
    }
}

cleanup 工作流程

effect.run()
    ↓
cleanup(effect)  →  移除所有旧依赖
    ↓
执行fn()         →  重新收集依赖
    ↓
完成执行

scheduler调度器

可调度性

可调度性是响应式系统中非常重要的特性,当触发更新重新执行副作用函数时,我们能决定副作用函数执行的时机、次数以及方式等,这就是可调度性

为什么需要调度器?

没有调度器的时候,可能会产生一些新的问题:

  • 频繁修改数据会导致effect执行多次,造成性能浪费
  • 有时需要控制effect的执行时机(如异步更新)
  • 需要批量处理更新,减少重复计算

如以下示例:

function demonstrateNoScheduler() {
    const state = reactive({ count: 0 });
    
    effect(() => {
        console.log(`   effect执行: count = ${state.count}`);
    });
    
    console.log('连续修改3次数据:');
    state.count = 1;
    state.count = 2;
    state.count = 3;
    
    console.log('   effect被执行了3次,可能是不必要的');
}

上述代码中,由于连续更改 state.count 的值,可能会导致 effect 被多次重复执行,这在很大程度上是不必要的。

通过 scheduler 调度器,可以自定义 effect 的执行策略,很好的解决上述问题。

调度器的基本实现

class EffectWithScheduler {
    constructor(fn, scheduler) {
        this.fn = fn;
        this.scheduler = scheduler;
        this.deps = [];
        this.active = true;
    }
    
    run() {
        if (!this.active) {
            return this.fn();
        }
        
        cleanup(this);
        activeEffect = this;
        effectStack.push(this);
        
        try {
            const result = this.fn();
            return result;
        } finally {
            effectStack.pop();
            activeEffect = effectStack[effectStack.length - 1];
        }
    }
    
    // 触发更新时调用
    trigger() {
        if (this.scheduler) {
            // 如果有调度器,交给调度器处理
            this.scheduler(this);
        } else {
            // 否则立即执行
            this.run();
        }
    }
}

调度器的应用场景

  1. 异步更新(Vue的nextTick),可以将多次同步更新合并为一次异步更新
  2. 使用 requestAnimationFrame 控制动画帧更新时机
  3. 控制高频更新的执行频率,防抖/节流
  4. 可以限制只有在特定条件下才执行 effect

懒执行effect(lazy)

为什么需要懒执行?

在有些场景下,我们并不希望 effect 副作用函数立即执行,而是希望它在需要的时候才执行,此处我们就可以通过 lazy 属性,即懒执行来实现。

懒执行场景

  1. 计算属性(computed)—— 只在被访问时才计算
  2. 需要手动控制的 effect
  3. 条件性执行的副作用
  4. 性能优化 —— 避免不必要的初始化计算

懒执行的实现

class LazyEffect {
    constructor(fn, options = {}) {
        this.fn = fn;
        this.lazy = options.lazy || false;
        this.scheduler = options.scheduler || null;
        this.deps = [];
        this.active = true;
    }
    
    run() {
        if (!this.active) {
            return this.fn();
        }
        
        cleanup(this);
        activeEffect = this;
        effectStack.push(this);
        
        try {
            const result = this.fn();
            return result;
        } finally {
            effectStack.pop();
            activeEffect = effectStack[effectStack.length - 1];
        }
    }
    
    trigger() {
        if (this.scheduler) {
            this.scheduler(this);
        } else {
            this.run();
        }
    }
}

停止effect(stop方法)

stop 的作用

  1. 组件卸载时,停止响应式依赖,避免内存泄漏
  2. 用户可以手动停止不需要的副作用
  3. 临时禁用某个响应式关系

stop 的实现

class StoppableEffect {
    constructor(fn, options = {}) {
        this.fn = fn;
        this.scheduler = options.scheduler || null;
        this.onStop = options.onStop || null; // 停止时的回调
        this.deps = [];
        this.active = true; // 是否活跃
    }
    
    run() {
        if (!this.active) {
            // 如果不活跃,只执行函数,不收集依赖
            return this.fn();
        }
        
        cleanup(this);
        activeEffect = this;
        effectStack.push(this);
        
        try {
            const result = this.fn();
            return result;
        } finally {
            effectStack.pop();
            activeEffect = effectStack[effectStack.length - 1];
        }
    }
    
    stop() {
        if (this.active) {
            console.log('   [stop] 停止effect');
            cleanup(this); // 清除所有依赖
            this.active = false;
            
            // 调用停止回调
            if (this.onStop) {
                this.onStop();
            }
        }
    }
    
    trigger() {
        if (this.scheduler) {
            this.scheduler(this);
        } else if (this.active) {
            this.run();
        }
    }
}

完整的ReactiveEffect类源码

// 全局变量
const targetMap = new WeakMap();
let activeEffect = null;
const effectStack = [];

// 清理函数
function cleanup(effect) {
    const { deps } = effect;
    if (deps.length) {
        console.log(`   [cleanup] 清除 ${effect.name || 'anonymous'}${deps.length} 个依赖`);
        deps.forEach(dep => dep.delete(effect));
        deps.length = 0;
    }
}

// 完整的ReactiveEffect类
class ReactiveEffect {
    constructor(fn, options = {}) {
        this.fn = fn;
        this.scheduler = options.scheduler || null;
        this.onStop = options.onStop || null;
        this.onTrack = options.onTrack || null;
        this.onTrigger = options.onTrigger || null;
        
        this.deps = [];
        this.active = true;
        this.name = fn.name || 'anonymous';
    }
    
    run() {
        if (!this.active) {
            return this.fn();
        }
        
        // 清理旧依赖
        cleanup(this);
        
        // 入栈
        effectStack.push(this);
        activeEffect = this;
        
        // 调试钩子
        if (this.onTrack) {
            // 实际会传入更详细的信息
        }
        
        console.log(`   [run] 开始执行 ${this.name}`);
        
        try {
            const result = this.fn();
            console.log(`   [run] ${this.name} 执行完成`);
            return result;
        } finally {
            // 出栈
            effectStack.pop();
            activeEffect = effectStack[effectStack.length - 1];
        }
    }
    
    stop() {
        if (this.active) {
            console.log(`   [stop] 停止 ${this.name}`);
            cleanup(this);
            this.active = false;
            
            if (this.onStop) {
                this.onStop();
            }
        }
    }
    
    // 触发更新(由响应式系统调用)
    trigger() {
        if (!this.active) return;
        
        if (this.scheduler) {
            console.log(`   [scheduler] ${this.name} 被调度`);
            this.scheduler(this);
        } else {
            this.run();
        }
    }
}

// 依赖收集
function track(target, key) {
    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);
        
        if (activeEffect.onTrack) {
            activeEffect.onTrack({
                effect: activeEffect,
                target,
                key,
                type: 'get'
            });
        }
        
        console.log(`   [track] ${activeEffect.name} 依赖了 ${String(key)}`);
    }
}

// 触发更新
function trigger(target, key) {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    
    const dep = depsMap.get(key);
    if (!dep) return;
    
    console.log(`   [trigger] ${String(key)} 变化,触发 ${dep.size} 个effect`);
    
    // 复制一份,避免遍历时修改Set
    const effects = new Set(dep);
    effects.forEach(effect => {
        if (effect !== activeEffect) {
            effect.trigger();
        }
    });
}

// 创建响应式对象
function reactive(obj) {
    return new Proxy(obj, {
        get(target, key) {
            track(target, key);
            return target[key];
        },
        set(target, key, value) {
            target[key] = value;
            trigger(target, key);
            return true;
        }
    });
}

// 主effect函数
function effect(fn, options = {}) {
    const _effect = new ReactiveEffect(fn, options);
    
    if (!options.lazy) {
        _effect.run();
    }
    
    const runner = _effect.run.bind(_effect);
    runner.effect = _effect;
    
    return runner;
}

结语

本篇文章主要介绍了 effect 副作用函数的高级特性,掌握这些特性,我们不仅能更好地理解 Vue3 的工作原理,还能在遇到性能问题时,知道如何优化 effect 的执行策略,甚至在需要时实现自己的响应式系统。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!