RxJS animationFrameScheduler意外停止工作问题排查咨询
RxJS 7.x animationFrameScheduler 偶发卡死导致防抖算子失效问题
更新:下文附临时规避方案,目前仍在探寻该问题的合理解释与正式解决方案。
问题描述
在使用RxJS 7.5.4版本的自定义算子时,遇到算子偶发完全停止工作的问题。替换为auditTime算子时问题同样复现,因此判断故障点为animationFrameScheduler。
初始自定义算子实现代码如下:
import { animationFrameScheduler, Observable, OperatorFunction } from 'rxjs'; import { debounceTime, tap } from 'rxjs/operators'; export function debounceAnimated<T>(): OperatorFunction<T, T> { return (source$: Observable<T>) => source$.pipe( tap(() => console.log('A')), debounceTime(0, animationFrameScheduler), tap(() => console.log('B')), ); }
运行表现
- 正常状态:大多数场景下算子运行正常,应用中调用该算子的逻辑可正确执行,控制台可见
debounceTime触发时预期的连续A日志后跟随连续B日志。 - 故障状态:触发鼠标滚轮缩放图表的用户操作时,控制台仅输出大量
A日志,B日志完全停止输出,下游Observable订阅也停止响应。异常发生后应用无法自行恢复,其余功能运行正常、CPU占用低,未使用该算子的组件不受影响。
补充背景
该功能已稳定运行数年,近期升级RxJS版本后原有算子实现失效,才尝试替换为当前实现。原有实现代码如下,该写法在当前RxJS版本下已无法实现防抖效果:
debounce(() => EMPTY.pipe(observeOn(scheduler)))
已尝试的临时规避方案
添加setInterval定时手动刷新调度器的逻辑后问题得到缓解,异常发生后5秒内调度器会被完全清空,功能恢复正常。经过多次滚轮缩放测试,发现异常时animationFrameScheduler中堆积了大量action但无逻辑处理,导致action一直处于休眠状态。
临时方案代码如下:
import { animationFrameScheduler, Observable, OperatorFunction } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; setInterval(() => { while (animationFrameScheduler.actions.length > 0) { animationFrameScheduler.flush(); } }, 5000); export function debounceAnimated<T>(): OperatorFunction<T, T> { return (source$: Observable<T>) => source$.pipe( debounceTime(0, animationFrameScheduler) ); }
该方案存在明显缺陷:轮询间隔过长会导致异常恢复延迟,间隔过短会产生不必要的性能开销,并非最优解。
根因分析
这是RxJS 7.x系列版本animationFrameScheduler的已知实现缺陷:
- 该调度器内部通过
requestAnimationFrame(简称rAF)注册回调触发任务队列清空,逻辑上做了优化:仅当任务队列从空变为非空时,才会注册新的rAF回调,队列已有待执行任务时不会重复注册。 - 高频触发滚轮缩放类操作时,浏览器会优先将主线程资源分配给合成层的缩放/滚动渲染,可能直接跳过、丢弃已经注册的rAF回调;若此时
debounceTime、auditTime这类算子因为新输入频繁取消旧任务、插入新任务,调度器会判定“已有rAF回调待执行”,不会重新注册新的rAF。 - 被丢弃的rAF回调永远不会执行,调度器队列就会永久处于“等待rAF触发”的状态,所有堆积的任务都不会执行,最终表现为下游订阅完全停摆。
手动调用animationFrameScheduler.flush()会直接绕过rAF触发逻辑,同步执行队列中所有堆积任务,因此可以临时恢复功能。
正式修复方案
不需要轮询flush,直接替换内置的animationFrameScheduler为带rAF丢失兜底的自定义调度器即可,代码如下:
import { Observable, OperatorFunction, asyncScheduler, SchedulerAction, SchedulerLike } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; // 带兜底的animationFrame调度器,解决rAF回调被浏览器丢弃导致的队列卡死问题 const safeAnimationFrameScheduler: SchedulerLike = { now: () => asyncScheduler.now(), schedule<T>(work: (this: SchedulerAction<T>, state?: T) => void, delay: number = 0, state?: T) { // 非0延迟直接走async调度器兜底 if (delay > 0) { return asyncScheduler.schedule(work, delay, state); } let rafId = 0; let timeoutId = 0; // 双保险:同时注册rAF和32ms超时兜底(两帧时间,避免普通帧延迟误触发) rafId = requestAnimationFrame(() => { clearTimeout(timeoutId); asyncScheduler.schedule(work, 0, state); }); timeoutId = window.setTimeout(() => { cancelAnimationFrame(rafId); asyncScheduler.schedule(work, 0, state); }, 32); const subscription = asyncScheduler.schedule(function (this: SchedulerAction<T>, s?: T) { cancelAnimationFrame(rafId); clearTimeout(timeoutId); work.call(this, s); }, delay, state); // 取消时同时清掉rAF和超时 subscription.add(() => { cancelAnimationFrame(rafId); clearTimeout(timeoutId); }); return subscription; } }; export function debounceAnimated<T>(): OperatorFunction<T, T> { return (source$: Observable<T>) => source$.pipe( debounceTime(0, safeAnimationFrameScheduler) ); }
该实现特性:
- 每次调度任务时同时注册rAF回调和32ms超时兜底
- 任意一个触发就执行任务,同时取消另一个
- 任务取消时同步清理rAF和超时句柄,避免内存泄漏
- 完全兼容原有RxJS调度器接口,不需要修改业务调用逻辑
- 无轮询开销,异常发生时最多延迟1帧即可自动恢复
内容的提问来源于stack exchange,提问作者HankScorpio
相关产品推荐
相关产品推荐

