如何实现带异步回调的requestAnimationFrame循环?
问题描述
我找了几个类似的问题,但要么理解不到位,要么问题/答案和我的场景不完全匹配。
先看一个正常运行的requestAnimationFrame循环示例:
const someList = []; function main() { // 启动requestAnimationFrame循环 requestAnimationFrame(update) } function update() { doSomething() // 递归调用update维持循环 requestAnimationFrame(update); } function doSomething() { someList.push(Math.floor(Math.random() * 10)); } main();
但当循环里需要调用带await的异步函数doSomethingAsynchronous时,update要改成async函数,这时候我不知道该怎么处理requestAnimationFrame的异步回调:
function main() { // 现在update是async函数且需要await,这行代码该怎么写? requestAnimationFrame(update) } async function update() { await doSomethingAsynchronous() // 同样,这里该怎么写? requestAnimationFrame(update); } async function doSomethingAsynchronous() { // 示例函数,实际需要await调用 } main();
我猜可能的写法有下面几种,但不确定是否要await requestAnimationFrame,也不清楚是否需要传入async回调:
await requestAnimationFrame(update) await requestAnimationFrame(async () => { await update(); }); requestAnimationFrame(async () => { await update(); });
补充场景信息
我是要结合TensorFlow.js做目标检测,具体场景:
- 有视频流
- 提取视频每一帧传入模型
- 模型的
predict是异步函数,返回物体边界框 - 需要把视频帧绘制到canvas,拿到边界框后再绘制到帧上方
期望的流程:
视频 -> 提取帧 -> (*) 将帧绘制到canvas -> 异步运行目标检测 -> 将模型返回的边界框绘制到canvas -> (*) 进入下一帧循环(注意:帧只绘制一次,不能重复绘制)
解决方案
核心结论
requestAnimationFrame本身不支持Promise,也不需要await它——它的作用是在下一帧渲染前调度回调执行。而你的async函数update可以直接作为回调传入,但要注意不要让异步操作阻塞帧循环的调度,同时保证帧绘制的逻辑正确。
正确的循环写法
针对你的TensorFlow.js目标检测场景,我们需要调整逻辑:先绘制视频帧(保证用户看到最新画面),再异步执行检测,检测完成后绘制边界框,然后调度下一帧的循环。这里的关键是:不要在update里等待检测完成后再调度下一帧,而是先调度下一帧,让帧循环保持流畅,检测完成后再绘制边界框。
function main() { // 直接启动循环,不需要await requestAnimationFrame(update); } async function update() { // 1. 先提取并绘制当前视频帧(只绘制一次) const currentFrame = extractVideoFrame(); // 假设这是提取帧的同步函数 drawFrameToCanvas(currentFrame); // 同步绘制帧到canvas // 2. 异步执行目标检测,这里不阻塞帧循环调度 (async () => { const boxes = await model.predict(currentFrame); // 检测完成后,在当前帧上绘制边界框 drawBoundingBoxes(boxes); })(); // 3. 立即调度下一帧的update,保证循环流畅 requestAnimationFrame(update); } // 辅助函数示例 function extractVideoFrame() { // 从视频元素提取当前帧的逻辑,比如用canvas截取 const video = document.getElementById('video'); const tempCanvas = document.createElement('canvas'); tempCanvas.width = video.videoWidth; tempCanvas.height = video.videoHeight; const ctx = tempCanvas.getContext('2d'); ctx.drawImage(video, 0, 0); return tempCanvas; } function drawFrameToCanvas(frame) { const mainCanvas = document.getElementById('main-canvas'); const ctx = mainCanvas.getContext('2d'); ctx.drawImage(frame, 0, 0, mainCanvas.width, mainCanvas.height); } function drawBoundingBoxes(boxes) { const mainCanvas = document.getElementById('main-canvas'); const ctx = mainCanvas.getContext('2d'); ctx.strokeStyle = 'red'; ctx.lineWidth = 2; boxes.forEach(box => { ctx.strokeRect(box.x, box.y, box.width, box.height); }); }
为什么这么写?
- requestAnimationFrame不需要await:它的回调是在下一帧渲染前执行,本身是异步调度,但不是Promise,所以await它没有意义。
- 分离帧绘制和检测逻辑:先绘制帧保证画面流畅,检测放在自执行异步函数里,不会阻塞下一帧的调度——这样即使检测耗时,视频帧的播放也不会卡顿,检测完成后再补画边界框。
- 避免重复绘制帧:每一帧只在
update开头绘制一次,检测完成后只画边界框,不会覆盖或重复绘制帧。
关于你猜测的写法的说明
await requestAnimationFrame(update):无效,因为requestAnimationFrame返回的是请求ID(数字),不是Promise,await会直接resolve这个数字,没有意义。await requestAnimationFrame(async () => { await update(); });:同样无效,requestAnimationFrame不返回Promise,await没用,而且这种写法会让update的执行被包裹,但不会改变帧循环的本质。requestAnimationFrame(async () => { await update(); });:可以运行,但没必要——async函数本身可以直接作为requestAnimationFrame的回调,因为requestAnimationFrame不关心回调的返回值(即使是Promise)。不过如果你的update里有await,直接传update也是可以的,但要注意不要让await阻塞下一帧的调度(比如不要把requestAnimationFrame放在await之后,否则检测耗时会导致帧循环卡顿)。
优化建议
如果检测速度跟不上帧速率(比如视频30fps,但检测只能达到10fps),可以加一个节流机制,避免同时运行多个检测任务:
let isDetecting = false; async function update() { const currentFrame = extractVideoFrame(); drawFrameToCanvas(currentFrame); // 如果当前没有检测任务,才启动新的检测 if (!isDetecting) { isDetecting = true; try { const boxes = await model.predict(currentFrame); drawBoundingBoxes(boxes); } finally { isDetecting = false; } } requestAnimationFrame(update); }
这样可以避免多个异步检测任务堆积,导致内存占用过高或绘制混乱。
内容的提问来源于stack exchange,提问作者Swiffy
相关产品推荐
相关产品推荐

