You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

能否将宏时间线多任务合并为微任务?Promise替代setTimeout失效排查

问题解答

1. 能否将宏时间线中的多个任务合并为单个微任务?

当然可以啦!咱们先回忆下JS事件循环的机制:宏任务(像setTimeout、I/O操作)和微任务(比如Promise回调、queueMicrotask)属于不同的任务队列。微任务有个关键特性——当前宏任务执行完后,下一个宏任务开始前,会把所有微任务队列里的任务清空。利用这个特性,我们完全可以把多个原本会触发多次宏任务的操作,合并成一次微任务执行,这样能减少像ReactsetState这类操作带来的重复重渲染,性能自然就上去了。

2. Promise版本代码无法工作的原因及修改方案

为啥你的Promise版本不行?

咱们先拆解下你写的代码逻辑:每次调用set,都会往promises数组塞一个Promise.resolve(),然后立刻调用Promise.all(promises).then(...)。这就导致了一个核心问题——每调用一次set,就会多一个then回调等着执行。比如你连续调用3次set,就会生成3个then回调,每个都会跑一遍_setState,等于完全没合并,反而还会触发多次更新,和你想要的合并效果背道而驰。

另外,你没有控制合并的时机:Promise.resolve()的回调确实会进微任务队列,但你每次加新Promise就立刻执行Promise.all,根本做不到“把当前所有set调用的状态收集完,只执行一次更新”。而原来的setTimeout是靠clearTimeout取消之前的定时器,只留最后一个,实现了“防抖”合并的效果,你的Promise版本完全没模拟这个核心逻辑。

修改后的正确写法

咱们要做的就是用微任务模拟setTimeout的防抖逻辑,加个标志位来标记是否已经有一个微任务在排队,这样多次调用时只会触发一次合并更新:

class StateSetter {
  cache = {};
  isPending = false; // 标记是否已有等待执行的微任务

  set = <T>({ _setState }: stateSetterParams) => <K extends keyof T>(newState: { [P in K]: T[P] }): void => {
    // 先把新状态合并到缓存里
    this.cache = { ...this.cache, ...newState };

    // 如果还没在等待微任务执行,就创建一个
    if (!this.isPending) {
      this.isPending = true;
      // 用queueMicrotask添加微任务,语义比Promise.resolve更清晰
      queueMicrotask(() => {
        // 一次性把所有缓存的状态合并更新
        _setState((prevState: T) => ({ ...prevState, ...this.cache }));
        // 执行完重置缓存和标志位,方便下一次合并
        this.cache = {};
        this.isPending = false;
      });
    }
  };
}

为啥这个写法能work?

  • isPending标志位:第一次调用set时,isPendingfalse,我们创建微任务并把它设为true;后续再调用set,因为isPendingtrue,就只会把状态合并到cache,不会再新建微任务。
  • queueMicrotask:这个API是专门用来添加微任务的,比Promise.resolve().then()更直接(本质上都是微任务,但queueMicrotask一看就知道是干啥的)。当当前宏任务的同步代码全执行完后,这个微任务就会触发,一次性把所有缓存的状态合并更新,完美实现了合并效果,而且因为是微任务,执行时机比setTimeout的宏任务早,效率更高。

内容的提问来源于stack exchange,提问作者Aero Wang

火山引擎 最新活动