JS requestAnimationFrame 底层实现

window.requestAnimationFrame()后浏览器会在下一次重绘前回调,前端在回调中再次调用该函数,则实现了与屏幕帧率一致的回调。

但发现 CPU 占用较高,内存也会缓慢增加,有些怀疑底层实现有问题。

底层实现

从 WebKit LocalDOMWindow::requestAnimationFrame入口查起。

  • MonitorManager 管理了一批 Monitor;
  • Monitor 管理了一批 displayID 相同的 MonitorClient;
  • Scheduler 继承 MonitorClient;

iOS 中 Monitor 是 DisplayRefreshMonitorIOS:

ini 复制代码
DisplayRefreshMonitorIOS::startNotificationMechanism
-m_handler = adoptNS([[WebDisplayLinkHandler alloc] initWithMonitor:this]);

@implementation WebDisplayLinkHandler
- (id)initWithMonitor:(DisplayRefreshMonitorIOS*)monitor {
    if (self = [super init]) {
        m_monitor = monitor;
        // Note that CADisplayLink retains its target (self), so a call to -invalidate is needed on teardown.
        m_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
        [m_displayLink addToRunLoop:WebThreadNSRunLoop() forMode:NSDefaultRunLoopMode];
        m_displayLink.preferredFramesPerSecond = DisplayLinkFramesPerSecond;
    }
    return self;
}
- (void)invalidate {
    [m_displayLink invalidate];
    m_displayLink = nullptr;
}

底层果然是 CADisplayLink 去实现重绘时机捕获,但这里 CADisplayLink 与 WebDisplayLinkHandler 形成了循环引用。继续排查:

scss 复制代码
    m_monitors.append(DisplayRefreshMonitorWrapper { WTFMove(monitor) });

    struct DisplayRefreshMonitorWrapper {
        ~DisplayRefreshMonitorWrapper() {
            if (monitor)
                monitor->stop();
        }
        RefPtr<DisplayRefreshMonitor> monitor;
    };

MonitorManager 添加 Monitor 时有个 Wrapper 类,而 Wrapper 在析构时会调用 Monitor 的 stop 函数,进而打破循环引用。

回调分发

分发起点:

ini 复制代码
void DisplayRefreshMonitorIOS::displayLinkCallbackFired() {
    displayLinkFired(m_currentUpdate);
    m_currentUpdate = m_currentUpdate.nextUpdate();
}

constexpr WebCore::FramesPerSecond DisplayLinkFramesPerSecond = 60;
m_currentUpdate = { 0, DisplayLinkFramesPerSecond };

currentUpdate 是个当前帧数与帧率的结构体:

arduino 复制代码
// Used to represent a given update. An value of { 3, 60 } indicates that this is the third update in a 1-second interval
// on a 60fps cadence. updateIndex will reset to zero every second, so { 59, 60 } is followed by { 0, 60 }.
struct DisplayUpdate {
    unsigned updateIndex { 0 };
    FramesPerSecond updatesPerSecond { 0 };
    DisplayUpdate nextUpdate() const  {
        return { (updateIndex + 1) % updatesPerSecond, updatesPerSecond };
    }
...

由此可见,requestAnimationFrame 的帧率上限最多 60。

后续的分发逻辑就没啥特殊的了。

结论

requestAnimationFrame 底层基于 CADisplayLink 实现,不存在内存泄露,帧率上限 60。

回调后涉及 JS 引擎对 Dom 树的刷新等,JS 是解释执行语言,每秒 60 次高频调用确实有些成本。而由于垃圾回收机制的滞后性,缓慢的内存增量可能仅是来不及回收。

JS 引擎层面的问题就不深究了,我们能做的就是尽量避免使用 requestAnimationFrame,若非要获取帧刷新回调,应该设计为按需触发循环,禁止时挂起。

相关推荐
油丶酸萝卜别吃2 小时前
OpenLayers 精确经过三个点的曲线绘制
javascript
ShallowLin2 小时前
vue3学习——组合式 API:生命周期钩子
前端·javascript·vue.js
Nejosi_念旧2 小时前
Vue API 、element-plus自动导入插件
前端·javascript·vue.js
麻芝汤圆3 小时前
MapReduce 入门实战:WordCount 程序
大数据·前端·javascript·ajax·spark·mapreduce
Peter 谭5 小时前
React Hooks 实现原理深度解析:从基础到源码级理解
前端·javascript·react.js·前端框架·ecmascript
周胡杰6 小时前
鸿蒙接入flutter环境变量配置windows-命令行或者手动配置-到项目的创建-运行demo项目
javascript·windows·flutter·华为·harmonyos·鸿蒙·鸿蒙系统
乌夷7 小时前
axios结合AbortController取消文件上传
开发语言·前端·javascript
wuyijysx9 小时前
JavaScript grammar
前端·javascript
学渣y11 小时前
React状态管理-对state进行保留和重置
javascript·react.js·ecmascript
_龙衣11 小时前
将 swagger 接口导入 apifox 查看及调试
前端·javascript·css·vue.js·css3