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

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

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

javascript 复制代码
const state = reactive({ ok: true, text: 'hello' });

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

上述代码中,当 state.ok 为 true 时,effect 依赖了 ok 和 text ; 当 state.ok 为 false 时,effect 只依赖了 ok ,此时 text 的依赖应该被清理掉!

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

分支切换与cleanup

什么是分支切换?

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

分支切换带来的问题

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

cleanup的作用

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

javascript 复制代码
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 函数,清理所有旧的依赖关系。以此确保依赖关系始终是最新的,避免无效更新:

javascript 复制代码
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 工作流程

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

scheduler调度器

可调度性

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

为什么需要调度器?

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

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

如以下示例:

javascript 复制代码
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 的执行策略,很好的解决上述问题。

调度器的基本实现

javascript 复制代码
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. 性能优化 ------ 避免不必要的初始化计算

懒执行的实现

javascript 复制代码
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 的实现

javascript 复制代码
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类源码

javascript 复制代码
// 全局变量
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 的执行策略,甚至在需要时实现自己的响应式系统。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

相关推荐
卸任3 小时前
Vibe coding工具使用心得
前端·ai编程
brien93 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance4 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M4 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写4 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李4 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易4 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding4 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马4 小时前
前沿视界
前端
kyriewen4 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员