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

相关推荐
瘦的可以下饭了28 分钟前
Day01-API
javascript
Nan_Shu_6141 小时前
学习:Vue (2)
javascript·vue.js·学习
一水鉴天3 小时前
整体设计 定稿 之24+ dashboard.html 增加三层次动态记录体系仪表盘 之2 程序 (Q208 之2)
开发语言·前端·javascript
二狗哈3 小时前
Cesium快速入门17:与entity和primitive交互
开发语言·前端·javascript·3d·webgl·cesium·地图可视化
GISer_Jing4 小时前
AI驱动营销增长:7大核心场景与前端实现
前端·javascript·人工智能
星光不问赶路人4 小时前
new Array() 与 Array.from() 的差异与陷阱
javascript·面试
T___T4 小时前
Vue 3 做 todos , ref 能看懂,computed 终于也懂了
前端·javascript·面试
cindershade4 小时前
JavaScript 事件循环机制详解及项目中的应用
前端·javascript
王霸天4 小时前
🚀 告别“变形”与“留白”:前端可视化大屏适配的终极方案(附源码)
前端·javascript
LYFlied4 小时前
Vue版本演进:Vue3、Vue2.7与Vue2全面对比
前端·javascript·vue.js