1. What is requestAnimationFrame
window.requestAnimationFrame()
tells the browser-you want to execute an animation, and ask the browser to call the specified callback function to update the animation before the next redraw. This method needs to pass in a callback function as a parameter, and the callback function will be executed before the next browser redraw.
According to the definition of MDNrequestAnimationFrame
is an API provided by the browser to redraw web pages by frame. Take a look at the following example first to understand how it is used and run:
const test = document.querySelector<HTMLDivElement>("#test")!;
let i = 0;
function animation() {
if (i > 200) return;
test.style.marginLeft = `${i}px`;
window.requestAnimationFrame(animation);
i++;
}
window.requestAnimationFrame(animation);
The above code 1s is executed about 60 times, because the refresh frequency of general screen hardware devices is 60Hz
, and then about 16.6ms
every time it is executed. When using requestAnimationFrame
, you only need to call it repeatedly to achieve animation effects.
At the same time, requestAnimationFrame
will return a request ID, which is a unique value in the callback function list. You can use cancelAnimationFrame
to cancel the callback function by passing in the request ID.
const test = document.querySelector<HTMLDivElement>("#test")!;
let i = 0;
let requestId: number;
function animation() {
test.style.marginLeft = `${i}px`;
requestId = requestAnimationFrame(animation);
i++;
if (i > 200) {
cancelAnimationFrame(requestId);
}
}
animation();
Figure 1 below is the execution result of the above example:
2. Confusion in the implementation of requestAnimationFrame
The way to use JavaScript to achieve animation can also use setTimeout
, the following is the implementation code:
const test = document.querySelector<HTMLDivElement>("#test")!;
let i = 0;
let timerId: number;
function animation() {
test.style.marginLeft = `${i}px`;
// 执行间隔设置为 0,来模仿 requestAnimationFrame
timerId = setTimeout(animation, 0);
i++;
if (i > 200) {
clearTimeout(timerId);
}
}
animation();
Here setTimeout
is set to 0 to imitate requestAnimationFrame
.
You can't see any difference from the way of code implementation alone, but from the specific implementation results below, you can see a clear gap.
Figure 2 below is the execution result setTimeout
For a complete example, stamp codesandbox .
It is obvious that setTimeout
much "faster" than the animation realized by requestAnimationFrame
What is the reason for this?
As you might have guessed, Event Loop
and requestAnimationFrame
have some special mechanisms during execution. Let’s explore the relationship between Event Loop
and requestAnimationFrame
3. Event Loop and requestAnimationFrame
Event Loop
(event loop) is a browser internal mechanism used to coordinate events, user interaction, scripting, rendering, and networking.
Event Loop
is also divided into several types in the browser:
window event loop
worker event loop
worklet event loop
Our main discussion here is window event loop
. Event Loop
controlled by the main thread in a rendering process of the browser.
3.1 task queue
A Event Loop
has one or more task queues
. A task queue
is a collection of a series of tasks.
Note: A task queue is a collection in the data structure, not a queue, because the event loop processing model will get the first runnable task from the selected task queue instead of making the first one The task is dequeued.
The above content comes from the HTML specification . What is confusing here is that it is obviously a collection, why is it called "queue" TT
3.2 task
A task can have multiple task sources
(task sources), which task sources are there? Take a look at Gerneric task sources :
- DOM manipulation task source, such as inserting an element into the document in a non-blocking manner
- User interaction task source, user operation (such as click) event
- Network task source, network I/O response callback
- history traversal task source, such as history.back()
In addition there like Timers
( setTimeout
, setInterval
etc.), IndexDB
operation is also task source
.
3.3 microtask
A event loop
has a microtask queue
, but this "queue" is indeed the " FIFO
" queue.
The specification does not specify which task sources are microtasks. Generally, the following are considered to be microtasks:
- promises
- MutationObserver
- Object.observe
- process.nextTick (This thing is the API of Node.js, so I won’t discuss it for now)
3.4 Event Loop Processing Process
- It is agreed that a runnable task must be included in the selected task queue (taskQueue). If there is no such task queue, skip to the microtasks step below.
- Make the oldest task (oldestTask) in taskQueue the first executable task, and then delete it from taskQueue.
- Set the oldestTask above as the running task in the event loop.
- Execute the oldestTask.
- Set the running task in the event loop to null.
- Execute microtasks checkpoints (that is, execute tasks in the microtasks queue).
- Set hasARenderingOpportunity to false.
- Update rendering.
- If it is currently
window event loop
and there is no task in the task queues and the microtask queue is empty, and the rendering timing variable hasARenderingOpportunity is false, execute the idle period (requestIdleCallback
). - Return to the first step.
The above is event loop
from the specification, some contents are omitted, the full version is here .
Generally speaking, event loop
is to constantly look for executable tasks in the task queues. If there is, push it to call stack
(execution stack) for execution, and update the rendering at the right time.
Figure 3 below ( source ) is event loop
running on the main thread of the browser:
As for what the main thread does, this is another grand topic. Interested students can check out the browser internal secret series article .
In the above specification, the rendering process is after the execution of the microtasks queue, and then take a look at the rendering process.
3.5 update rendering
- To traverse all documents in the current browsing context, each document must be processed in the order found in the list.
- Rendering opportunities (Rendering opportunities) : If there is no rendering opportunity in the current browsing context, delete all docs and cancel rendering (whether there is a rendering opportunity here is determined by the browser itself, according to hardware refresh rate restrictions, page performance or whether the page is Factors in the background).
- If the current document is not empty, set hasARenderingOpportunity to true.
- Unnecessary rendering: If the browser thinks that the rendering of the browsing context of the updated document will not produce a visible effect and the animation frame callbacks of the document are empty, the rendering is canceled. (Finally saw the figure of requestAnimationFrame
- Remove the docs from the docs that the browser thinks is best to skip updating the rendered docs for other reasons.
- If the browsing context of the document is the top-level browsing context, refresh the auto-focus candidates of the document.
- To handle the resize event, pass in a performance.now() timestamp.
- To handle the scroll event, pass in a performance.now() timestamp.
- To process media queries, pass in a performance.now() timestamp.
- To run the CSS animation, pass in a performance.now() timestamp.
- To handle full screen events, pass in a performance.now() timestamp.
- executes the requestAnimationFrame callback and passes in a performance.now() timestamp .
- Execute the intersectionObserver callback and pass in a performance.now() timestamp.
- Draw each document.
- Update the ui and present it.
Figure 4 below ( source ) is a relatively clear flow of this process:
At this point, the requestAnimationFrame
of the callback of 0618cf0bd25768 is clear, and it will be style/layout/paint
before 0618cf0bd2576a.
setTimeout
requestAnimationFrame
animation is faster than the 0618cf0bd25781 animation mentioned at the beginning of the article, this is a good explanation.
First of all, the browser rendering has a rendering opportunity problem, that is, the browser will judge whether to render according to the current browsing context. It will try to be as efficient as possible and only render when necessary. If there is no interface change will not be rendered. According to the specification, because of hardware refresh frequency limitations, page performance, and whether the page has a background, etc., it is possible setTimeout
has been executed, it is found that the rendering time has not yet arrived, so setTimeout
back several times. Before rendering, the difference between the marginLeft set at this time and the marginLeft before the last rendering should be greater than 1px.
Figure 5 setTimeout
execution of 0618cf0bd257cb. The red circle shows two renderings, and the middle four times are for processing setTimout task
. Because the screen refresh rate is 60 Hz, so it is approximately 16.6ms after the execution of setTimeout task
for multiple times before the rendering time came. Perform rendering.
requestAnimationFrame
difference between the 0618cf0bd257f3 frame animation is that it will be called before each rendering. The difference between the marginLeft set at this time and the marginLeft before the last rendering is 1px.
Figure 6 requestAnimationFrame
execution of 0618cf0bd25804. Rendering will be performed after each call:
So it seems that setTimeout
"faster".
4. Implementation of different browsers
The above example is in Chrome
under test, the results of this basic example presented in all browsers are the same, look at the following example, it comes from Jake archilbald earlier proposed in 2017 this problem :
test.style.transform = 'translate(0, 0)';
document.querySelector('button').addEventListener('click', () => {
const test = document.querySelector('.test');
test.style.transform = 'translate(400px, 0)';
requestAnimationFrame(() => {
test.style.transition = 'transform 3s linear';
test.style.transform = 'translate(200px, 0)';
});
});
The execution of this code in Chrome and Firefox is shown in Figure 7:
To explain briefly, in this example, requestAnimationFrame
overrides the transform set in the click listener, because requestAnimationFrame
is called before the css (style) is calculated, so the animation moves 200 px to the right.
Note: The above code is executed in Chrome hidden mode. When your Chrome browser has many plug-ins or opens many tabs, it may also slide from right to left.
The implementation in safari is as shown in Figure 8:
Edge also had the same execution result as safari before, but it has been fixed now.
The reason for this result is that safari executes the requestAnimationFrame callback at 1 frame after rendering, so the requestAnimationFrame called in the current frame will render in the next frame. So safari first rendered the position to the right 400px position, and then moved to the left 200px position.
Regarding a more detailed explanation of the execution mechanism of event loop and requestAnimationFrame
special lecture , I recommend you to take a look.
5. Other enforcement rules
Continuing to look at the example that jake put forward, if under the implementation of the standard specification, what do you need to do if you want to achieve the effect presented by safari (that is, move from right to left)?
The answer is to add another layer of requestAnimationFrame
call:
test.style.transform = 'translate(0, 0)';
document.querySelector('button').addEventListener('click', () => {
const test = document.querySelector('.test');
test.style.transform = 'translate(400px, 0)';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
test.style.transition = 'transform 3s linear';
test.style.transform = 'translate(200px, 0)';
});
});
});
The execution result of the above code is consistent with safari, because requestAnimationFrame
is executed only once per frame, and the newly defined requestAnimationFrame
will be executed before the next frame is rendered.
6. Other applications
From the above example, we know that the use of setTimeout
to perform visual changes such as animation is likely to cause frame loss and setTimeout
. Therefore, you should try to avoid using 0618cf0bd25a24 to perform animation. It is recommended to use requestAnimationFrame
to replace it.
requestAnimationFrame
only used to achieve animation effects, but also can be used to implement split execution of large tasks.
From the rendering flowchart in Figure 4, we can know that: executes JavaScript task before rendering. If JavaScript executes for too long within a frame, rendering will be blocked, which will also cause frame .
In response to this situation, the JavaScript task can be divided into small blocks, and use requestAnimationFrame()
to run on each frame. As shown in the following example ( source ):
var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTime) {
var taskFinishTime;
do {
// 假设下一个任务被压入 call stack
var nextTask = taskList.pop();
// 执行下一个 task
processTask(nextTask);
// 如何时间足够继续执行下一个
taskFinishTime = window.performance.now();
} while (taskFinishTime - taskStartTime < 3);
if (taskList.length > 0) {
requestAnimationFrame(processTaskList);
}
}
7. Reference materials
internal secrets of modern browsers
JavaScript main thread. Dissected.
requestAnimationFrame Scheduling For Nerds
Welcome to follow the blog of Lab: 1618cf0bd25b9b aotu.io
Or follow the AOTULabs official account (AOTULabs) and push articles from time to time.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。