JavaScript事件循环(下) - requestAnimationFrame与Web Workers

JavaScript事件循环(下) - requestAnimationFrame与Web Workers

如何实现丝滑流畅的 60fps 动画?如何在单线程 JavaScript 中实现真正的并行计算?本篇文章将探索事件循环的高阶应用。

前言:从60fps的动画说起

在 JavaScript 中,常见的动画实现方式有以下三种:

使用setInterval(不推荐)

javascript 复制代码
function animateWithSetInterval() {
    setInterval(() => {
        updateAnimation();
        renderFrame();
    }, 16.67); 
}

上述代码试图达到60fps(1000/60 ≈ 16.67ms),但定时器不精确,可能丢帧或过度绘制。

递归setTimeout

javascript 复制代码
function animateWithSetTimeout() {
    function loop() {
        updateAnimation();
        renderFrame();
        setTimeout(loop, 16.67);
    }
    loop();
}

这种方式比 setInterval 稍好,但仍可能和屏幕刷新不同步。

使用requestAnimationFrame(推荐)

javascript 复制代码
function animateWithRAF() {
    function loop(timestamp) {
        updateAnimation(timestamp);
        renderFrame();
        requestAnimationFrame(loop);
    }
    requestAnimationFrame(loop);
}

优势:自动匹配屏幕刷新率,节省资源。

requestAnimationFrame:动画的黄金标准

什么是requestAnimationFrame?

requestAnimationFrame(简称 rAF) 是浏览器专门为动画和连续视觉更新提供的 API。它的核心特点是:

  • 在浏览器下一次重绘之前调用指定的回调函数,确保动画与屏幕刷新同步。

rAF的基本用法

javascript 复制代码
function animate() {
    // 更新动画状态
    updateAnimation();
    
    // 渲染当前帧
    renderFrame();
    
    // 请求下一帧
    requestAnimationFrame(animate);
}

// 启动动画循环
requestAnimationFrame(animate);

rAF的优势

  1. 自动匹配显示器刷新率(通常是60Hz)
  2. 页面不可见时自动暂停,节省资源
  3. 浏览器可以优化动画性能
  4. 提供精确的时间戳参数

rAF的工作原理

javascript 复制代码
function experimentRAF() {
    console.log('实验开始');
    
    // 记录帧数
    let frameCount = 0;
    let lastTimestamp = 0;
    
    function frameCallback(timestamp) {
        frameCount++;
        
        // 计算帧间隔
        if (lastTimestamp > 0) {
            const interval = timestamp - lastTimestamp;
            console.log(`第${frameCount}帧,间隔: ${interval.toFixed(2)}ms`);
        }
        
        lastTimestamp = timestamp;
        
        if (frameCount < 10) {
            requestAnimationFrame(frameCallback);
        } else {
            console.log('实验结束,平均帧率:', (1000 / ((timestamp - startTime) / 10)).toFixed(1), 'fps');
        }
    }
    
    const startTime = performance.now();
    requestAnimationFrame(frameCallback);
}

rAf 的关键点在于:frameCallback() 回调中的 timestamp 参数,这个 timestampperformance.now() 返回的高精度时间,也表示回调开始执行的时间。

rAF在事件循环中的位置

javascript 复制代码
setTimeout(() => {
    console.log('1. setTimeout - 宏任务');
    
    Promise.resolve().then(() => {
        console.log('2. setTimeout中的微任务');
    });
}, 0);

Promise.resolve().then(() => {
    console.log('3. Promise - 微任务');
    
    requestAnimationFrame(() => {
        console.log('4. Promise中注册的rAF');
    });
});

requestAnimationFrame(() => {
    console.log('5. 直接注册的rAF');
    
    setTimeout(() => {
        console.log('6. rAF中注册的setTimeout');
    }, 0);
});

queueMicrotask(() => {
    console.log('7. queueMicrotask - 微任务');
});

console.log('8. 同步代码');

上述代码的输出顺序如下:

  • 8.同步代码
  • 3.Promise - 微任务
  • 7.queueMicrotask - 微任务
  • 1.setTimeout - 宏任务
  • 2.setTimeout中的微任务
  • 5.直接注册的rAF
  • 4.Promise中注册的rAF
  • 6.rAF中注册的setTimeout

其执行过程如下:

  1. 执行宏任务 (setTimeout, 事件回调等)
  2. 执行微任务 (Promise, queueMicrotask等)
  3. 执行rAF回调 (动画更新)
  4. 样式计算和布局
  5. 绘制 (Paint)
  6. 合成 (Composite)
  7. 检查空闲,执行 requestIdleCallback 回调

Web Workers:真正的多线程编程

什么是Web Workers?

Web Workers 允许 JavaScript 在后台线程中运行脚本,而不会阻塞主线程。这意味着我们可以执行CPU密集型任务,而不会影响页面的响应性。

javascript 复制代码
// 主线程代码
console.log('主线程: 开始');

// 创建一个Worker
const worker = new Worker('worker.js');

// 向Worker发送消息
worker.postMessage({
    type: 'CALCULATE',
    data: { numbers: [1, 2, 3, 4, 5] }
});

// 接收Worker的消息
worker.onmessage = (event) => {
    const result = event.data;
    console.log('主线程: 收到Worker结果', result);
    
    // 更新UI
    document.getElementById('result').textContent = `结果: ${result}`;
};

// 处理Worker错误
worker.onerror = (error) => {
    console.error('Worker错误:', error);
};

console.log('主线程: 继续执行其他任务...');

Worker的限制:

  1. 无法访问DOM
  2. 无法使用window、document等
  3. 不能执行同步的XHR(可以使用fetch)
  4. 有同源策略限制
  5. 不能加载本地文件(file://协议)

Web Workers的类型

1. 专用Worker (Dedicated Worker)

只能被创建它的脚本使用:

javascript 复制代码
const dedicatedWorker = new Worker('dedicated-worker.js');

2. 共享Worker (Shared Worker)

可以被多个脚本共享(同源):

javascript 复制代码
if (window.SharedWorker) {
    const sharedWorker = new SharedWorker('shared-worker.js');
    
    // 通过port通信
    sharedWorker.port.onmessage = (event) => {
        console.log('收到共享Worker消息:', event.data);
    };
    
    sharedWorker.port.postMessage('Hello Shared Worker');
} else {
    console.log('浏览器不支持Shared Worker');
}

3. Service Worker

用于离线缓存、推送通知等:

javascript 复制代码
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js')
        .then(registration => {
            console.log('Service Worker注册成功:', registration);
        })
        .catch(error => {
            console.error('Service Worker注册失败:', error);
        });
}

4. Audio Worklet (Chrome 66+)

用于高性能音频处理:

javascript 复制代码
if (window.audioContext && window.audioContext.audioWorklet) {
    audioContext.audioWorklet.addModule('audio-processor.js')
        .then(() => {
            console.log('Audio Worklet加载成功');
        });
}

5. Paint Worklet (CSS Houdini)

用于自定义CSS绘制:

javascript 复制代码
if (CSS.paintWorklet) {
    CSS.paintWorklet.addModule('paint-worklet.js')
        .then(() => {
            console.log('Paint Worklet加载成功');
        });
}

requestIdleCallback:空闲期任务调度

什么是requestIdleCallback?

requestIdleCallback (简称 rIC )允许开发者在浏览器空闲时期调度任务。这对于执行低优先级或非紧急的工作非常有用,避免影响关键的用户交互和动画。

javascript 复制代码
const idleCallbackId = requestIdleCallback((deadline) => {
    console.log('空闲回调开始执行');
    
    // deadline对象包含重要信息:
    console.log('剩余时间:', deadline.timeRemaining(), 'ms');
    console.log('是否超时:', deadline.didTimeout);
    
    // 在空闲时间内执行任务
    while (deadline.timeRemaining() > 0 && hasMoreWork()) {
        doSomeLowPriorityWork();
    }
    
    // 如果还有工作未完成,再次安排
    if (hasMoreWork()) {
        requestIdleCallback(processLowPriorityWork);
    }
    
    console.log('空闲回调结束');
}, { timeout: 1000 }); // 设置超时,确保在1秒内执行

// 主线程继续执行其他任务
console.log('主线程继续执行...');

rIC的关键特点:

  1. 只在浏览器空闲时执行
  2. 提供deadline对象,包含剩余时间信息
  3. 可以设置timeout确保执行
  4. 适合低优先级、可中断的任务

rIC在事件循环中的位置

javascript 复制代码
// 理解rIC的执行时机
console.log('=== 事件循环中各API的执行时机 ===');

setTimeout(() => {
    console.log('1. setTimeout - 宏任务');
}, 0);

Promise.resolve().then(() => {
    console.log('2. Promise - 微任务');
});

requestAnimationFrame(() => {
    console.log('3. requestAnimationFrame - 动画帧回调');
    
    // 在rAF中安排rIC
    requestIdleCallback(() => {
        console.log('5. rAF中安排的rIC - 空闲回调');
    }, { timeout: 100 });
});

requestIdleCallback(() => {
    console.log('4. 直接安排的rIC - 空闲回调');
    
    // 在rIC中安排微任务
    Promise.resolve().then(() => {
        console.log('6. rIC中的Promise - 微任务');
    });
}, { timeout: 100 });

queueMicrotask(() => {
    console.log('7. queueMicrotask - 微任务');
});

console.log('8. 同步代码');

上述代码的输出顺序如下:

  • 8.同步代码
  • 2.Promise - 微任务
  • 7.queueMicrotask - 微任务
  • 1.setTimeout - 宏任务
  • 3.requestAnimationFrame - 动画帧回调
  • 4.直接安排的rIC - 空闲回调
  • 6.rIC中的Promise - 微任务
  • 5.rAF中安排的rIC - 空闲回调

其执行过程如下:

  1. 执行宏任务 (setTimeout, 事件回调等)
  2. 执行微任务 (Promise, queueMicrotask等)
  3. 执行rAF回调 (动画更新)
  4. 样式计算和布局
  5. 绘制 (Paint)
  6. 合成 (Composite)
  7. 检查空闲时间,如果有空闲,则执行rIC回调;否则等待下一帧。

核心概念总结

requestAnimationFrame (rAF):

  • 是什么:浏览器提供的动画API,在每次重绘前执行回调
  • 为什么用:自动匹配显示器刷新率,页面不可见时暂停,节省资源
  • 最佳时机:视觉更新、动画、连续状态变化
  • 执行位置:在微任务之后,重绘之前

Web Workers:

  • 是什么:允许JavaScript在后台线程运行的技术
  • 为什么用:执行CPU密集型任务而不阻塞主线程
  • 限制:无法访问DOM,通过消息传递通信
  • 类型:专用Worker、共享Worker、Service Worker等

requestIdleCallback (rIC):

  • 是什么:在浏览器空闲时调度任务的API
  • 为什么用:执行低优先级、非紧急任务
  • 关键对象:deadline包含剩余时间和超时信息
  • 执行位置:在一帧的最后,如果有空闲时间

结语

本文简单介绍了requestAnimationFrameWeb WorkersrequestIdleCallback 的基本用法和对比,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

相关推荐
我是ed.2 小时前
Vue3 音频标注插件 wavesurfer
前端·vue.js·音视频
Vallelonga2 小时前
Rust Option.as_ref() 方法
开发语言·rust
MarkHD2 小时前
Python RPA入门实战:深入解析RPA核心概念与Python的优势(第5-6天)
开发语言·python·rpa
Konata122 小时前
实现进阶的C/S通信
java·开发语言
初听于你2 小时前
Java 泛型详解
java·开发语言·windows·java-ee
铁蛋AI编程实战2 小时前
Gemini in Chrome 全实战:解锁+API调用+自定义扩展+本地推理
前端·人工智能·chrome
rainbow68892 小时前
Java17新特性深度解析
java·开发语言·python
bin91532 小时前
C盘瘦身大作战:程序员的存储空间优化全攻略
c语言·开发语言·c盘清理·c盘清理技巧分享
小秋学嵌入式-不读研版2 小时前
智能台灯功能重设计方案
开发语言