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,若非要获取帧刷新回调,应该设计为按需触发循环,禁止时挂起。

相关推荐
quan26316 分钟前
Vue实践篇-02,AI生成代码
前端·javascript·vue.js
小桥风满袖1 小时前
极简三分钟ES6 - 类与继承
前端·javascript
子兮曰1 小时前
🚀99% 的前端把 reduce 用成了「高级 for 循环」—— 这 20 个骚操作让你一次看懂真正的「函数式折叠」
前端·javascript·typescript
wifi歪f1 小时前
📦 qiankun微前端接入实战
前端·javascript·面试
小桥风满袖1 小时前
极简三分钟ES6 - Symbol
前端·javascript
子兮曰1 小时前
🚀Map的20个神操作,90%的开发者浪费了它的潜力!最后的致命缺陷让你少熬3天夜!
前端·javascript·ecmascript 6
练习时长两年半的Java练习生(升级中)1 小时前
从0开始学习Java+AI知识点总结-30.前端web开发(JS+Vue+Ajax)
前端·javascript·vue.js·学习·web
良木林1 小时前
JS中正则表达式的运用
前端·javascript·正则表达式
芭拉拉小魔仙2 小时前
【Vue3+TypeScript】H5项目实现企业微信OAuth2.0授权登录完整指南
javascript·typescript·企业微信
吃饭睡觉打豆豆嘛3 小时前
深入剖析 Promise 实现:从原理到手写完整实现
前端·javascript