@MikeGledhill 的答案(已被删除)本质上是答案的开始,尽管它可以更好地解释它,并且浏览器当时可能并不都有requestAnimationFrame
可用的 API:
像素的绘制发生在下一个动画帧中。这意味着如果您调用drawImage
,屏幕像素实际上不会在那个时候更新,而是在下一个动画帧中更新。
这没有什么活动。
但!我们可以使用requestAnimationFrame
在绘制(显示更新)发生之前为下一帧安排回调:
myImg.onload = function() {
myContext.drawImage(containerImg, 0, 0, 300, 300);
requestAnimationFrame(() => {
// This function will run in the next animation frame, *right before*
// the browser will update the pixels on the display (paint).
// To ensure that we run logic *after* the display has been
// updated, an option is to queue yet one more callback
// using setTimeout.
setTimeout(() => {
// At this point, the page rendering has been updated with the
// `drawImage` result (or a later frame's result, see below).
}, 0)
})
};
这里发生了什么:
该requestAnimtionFrame
调用安排了一个函数,该函数将在浏览器更新显示像素之前被调用。本次回调完成后,浏览器会在后续的tick中继续同步更新显示像素,非常类似于微任务。
类似于浏览器更新显示的“微任务”,发生在您的requestAnimationFrame
回调之后,并且发生在Promise.resolve().then()
用户使用或await
语句在回调中创建的所有用户创建的微任务之后。这意味着不能在绘制任务发生后立即(同步)触发延迟代码。
保证在下一个绘制任务之后触发逻辑的唯一方法是使用setTimeout
(或postMessage
技巧)从动画帧回调中将宏任务(而不是微任务)排队。从requestAnimationFrame
回调排队的宏任务将在所有微任务和类似微任务的任务之后触发,包括更新像素的任务。setTimeout(或 postMessage)宏任务不会在动画帧微任务之后同步触发。
这种方法虽然并不完美。大多数情况下,从setTimeout
(更可能是)排队的宏任务将在下一个动画帧和绘制周期之前postMessage
触发。但是,由于(and ) 的规范,不能保证延迟将与我们指定的完全相同(在此示例中),并且浏览器可以自由使用启发式和/或硬编码值(如 2ms)来确定何时是运行(宏任务)回调的最快时间。setTimeout
postMessage
0
setTimeout
由于宏任务调度的这种非保证的非同步性质,虽然实际上不太可能,但您的setTimeout
(或postMessage
)回调可能不仅在当前动画帧(以及更新显示的绘制周期)之后触发,而且在下一个动画帧(及其绘制任务)之后,这意味着宏任务回调对于您所针对的帧而言触发的可能性很小。使用postMessage
代替时,此几率会降低setTimeout
。
话虽如此,除非您尝试编写捕获绘制像素并将它们与预期结果或类似结果进行比较的测试,否则您可能不应该做这种事情。
通常,您应该使用 来安排任何绘图逻辑(fe ctx.drawImage()
)requestAnimationFrame
,永远不要依赖绘制更新的实际时间,并假设用户将看到浏览器 API 保证您指定他们看到的内容(浏览器有自己的自己的测试以确保其 API 工作)。
最后,我们不知道您的实际目标是什么。这个答案很可能与该目标无关。
postMessage
这是使用技巧的相同示例:
let messageKey = 0
myImg.onload = function() {
myContext.drawImage(containerImg, 0, 0, 300, 300);
requestAnimationFrame(() => {
// This function will run in the next animation frame, *right before*
// the browser will update the pixels on the display (paint).
const key = "Unique message key for after paint callback: "+ messageKey++
// To ensure that we run logic *after* the display has been
// updated, an option is to queue yet one more callback
// using postMessage.
const afterPaint = (event) => {
// Ignore interference from any other messaging in the app.
if (event.data != key) return
removeEventListener('message', afterPaint)
// At this point, the page rendering has been updated with the
// `drawImage` result (or a later frame's result, but
// more unlikely than with setTimeout, as per above).
}
addEventListener('message', afterPaint)
// Hack: send a message which arrives back to us in a
// following macrotask, more likely sooner than with
// setTimeout.
postMessage(key, '*')
})
};