Do you really understand the JS event loop?


Original link

For the browser, there are multiple threads working together, as shown in the figure below. For details, please refer to a frame analysis .
insert image description here

It also refers to the often-called JS single-threaded engine Main Therad.

main-thread

Note that every block of the above main thread may not be executed, it depends on the actual situation.
First, the process of Parse HTML-> Compositeis called the rendering pipeline flow Rendering pipeline.

There is a non-stop polling mechanism inside the browser to check whether there are tasks in the task queue, and if so, take them out and hand them over JS引擎to execute.

For example:

const foo = () => console.log("First");
const bar = () => setTimeout(() => console.log("Second"), 500);
const baz = () => console.log("Third");

bar();
foo();
baz();

process:
insert image description here

Task Queue Tasks Queue

Some common webapiwill generate a taskand send it to the task queue.

  • scriptLabel
  • XHR, addEventListenerand other event callbacks
  • setTimeouttimer

Each taskexecution in a poll has its own context and does not affect each other. That's why, scriptthe code in the tag crashes and does not affect the subsequent scriptcode execution.

  • The polling pseudocode is as follows (used in the original video pop, which is JSerconvenient for the world view of shift)
while(true) {
    
    
  task = taskQueue.shift();
  execute(task);
}
  • Task queues are not necessarily maintained in one queue, for example input event, may besetTimeout maintained in different queues. If the code operates , the main thread will also execute the rendering pipeline flow. The pseudocode is modified as follows:callback
    DOM
while(true) {
+  queue = getNextQueue();
-  task = taskQueue.shift();
+  task = queue.shift();
  execute(task);
  
+  if(isRepaintTime()) repaint();
}
  • for example
button.addEventListener('click', e => {
    
    
  while(true);
});

Click buttonto generate one task. When the task is executed, the main thread has been occupied and stuck, and the task cannot be exited, resulting in the inability to respond to user interaction or render dynamic graphics.

Replace and execute the following code

function loop() {
    
    
  setTimeout(loop, 0);
}
loop();

Executes in a seemingly infinite loop loop, setTimeoutyielding one when the time expires task. loopExit the main thread after execution . Enables user interaction events and rendering to be performed.

Because of this, execution of setTimeoutand other webapispawned taskdepends on the order in the task queue.
Even if there are no other tasks in the task queue, it cannot be 0秒executed . setTimeoutWhen the timer cbexpires , it will be put into the task queue, and it will be taken out taskby the polling engine for execution, at least approximately 4.7ms.

requestAnimationFrame

  • For example, keep moving a box forward by 1 pixel
function callback() {
    
    
  moveBoxForwardOnePixel();
  requestAnimationFrame(callback);
}

callback()

replace withsetTimeout

function callback() {
  moveBoxForwardOnePixel();
-  requestAnimationFrame(callback);
+  setTimeout(callback, 0);
}

callback()

In contrast, it can be found that setTimeoutthe movement is rAFsignificantly faster than the movement (about 3.5 times).
It means that setTimeoutthe callback is too frequent, which is not a good thing.

Rendering pipeline flow doesn't necessarily happen between each setTimeoutspawned by task, but can also happen after multiple setTimeoutcallbacks .
It's up to the browser to decide when to render and as efficiently as possible, it will only render if it's worth updating, and not if it doesn't.

If the browser is running in the background and nothing is displayed, the browser won't render because it doesn't make sense. In most cases, the page will be refreshed at a fixed frequency
to ensure that 60FPSthe human eye feels smooth, that is, about one frame 16ms. If the frequency is high, the human eye cannot see meaningless, and if it is lower than the human eye, it can detect stuttering.

When the main thread is very idle, setTimeoutthe callback can be executed every 4msabout , leaving 2msthe rendering pipeline flow, which setTimeoutcan be executed within one frame 3.5次.
3.5ms * 4 + 2ms = 16ms.

setTimeout

setTimeoutThere are too many calls 3-4次, more than the user can see, and more than the browser can display, about 3/4 is wasted.
Many old animation libraries are used setTimeout(animFrame, 1000 / 60)for optimization.
insert image description here

But setTimeoutit is not born for animation, the execution is unstable, it will cause drift or the heavy task will delay the rendering pipeline flow.

broken

requestAnimationFrameIt's there to fix these issues, keeping everything neat and organized and every frame happening on time.

happy

It is recommended to use requestAnimationFrameWrap animation work to improve performance. It solves the problem of setTimeoutuncertainty and performance waste, and it is up to the browser to ensure that it is executed before rendering the pipeline flow.

  • A confusing question: Can the following code 0pxmove to 1000pxto and then to 500pxto ?
button.addEventListener('click', () => {
    
    
  box.style.transform = 'translateX(1000px)';
  box.style.transition = 'transform 1s ease-in-out';
  box.style.transform = 'translateX(500px)';
});

Result: 0pxMoved 500pxto . Since the code block of the callback task is executed synchronously, the browser does not care about the intermediate state.

  • Modify as follows
button.addEventListener('click', () => {
  box.style.transform = 'translateX(1000px)';
  box.style.transition = 'transform 1s ease-in-out';
-  box.style.transform = 'translateX(500px)';

+  requestAnimationFrame(() => {
+    box.style.transform = 'translateX(500px)';
+  });
});

Result: Still 0pxmoving 500pxto .

This is addEventListenerbecause taskthe synchronous code in the is modified to 1000px.
Before the computed style in the rendering pipeline flow is executed, it needs to be executed rAF, and the final style is 500px.

  • Modified correctly, before the execution of the next frame's rendering pipeline flow 500px.
button.addEventListener('click', () => {
  box.style.transform = 'translateX(1000px)';
  box.style.transition = 'transform 1s ease-in-out';

  requestAnimationFrame(() => {
-    box.style.transform = 'translateX(500px)';
+    requestAnimationFrame(() => {
+        box.style.transform = 'translateX(500px)';
+    });
  });
});
  • Not a good way, but it works
button.addEventListener('click', () => {
  box.style.transform = 'translateX(1000px)';
  box.style.transition = 'transform 1s ease-in-out';
+  getComputedStyle(box).transform;
  box.style.transform = 'translateX(500px)';
});

getComputedStyleIt will lead to forced rearrangement, the rendering pipeline flow is executed in advance, and redundant operations will consume performance.

  • bad news

Edgeand Safariof rAFdo not conform to the specification, and are wrongly executed after the rendering pipeline flow.

Microtasks

DOMNodeInsertedOriginally designed to listen DOMfor changes to .

  • For example, the following code will trigger how many times DOMNodeInserted.
document.body.addEventListener('DOMNodeInserted', () => {
    
    
  console.log('Stuff added to <body>!');
});

for(let i = 0; i < 100; i++) {
    
    
  const span = document.createElement('span');
  document.body.appendChild(span);
  span.textContent = 'hello';
}

Ideally after the for loop finishes, DOMNodeInsertedthe callback executes once.
Result: executed 200times . Add spantrigger 100times, set textContenttrigger 100.
This DOMNodeInsertedmakes a very poor performance burden.
To solve these problems, a new task queue called microtasks was created Microtasks.

Common Microtasks

  1. MutationObserver - Observer of DOM mutation events.
  2. Promise
  3. process.nextTick (node 中)

The microtask is taken out in an event polling and taskthe execution is completed, that is JavaScript, there is no executable content in the running stack (stack).
The browser then fetches all microtasksthe to execute.

  • loopWhat if you create a like the previous with microtasks ?
function loop() {
    
    
  Promise.resolve().then(loop);
}

loop();

You will find that it freezes whilejust like .

Now we have 3 queues of different nature

  1. task queue
  2. rAF tail
  3. microtask queue
  • The task queue is known before, and an taskexecution , and if it is generated new task, it is put into the queue. taskAfter the execution is completed, wait for the next polling to take out next task.
  • After the microtask queue task is executed, execute all tasks in the queue microtask. If generated new microtask, enter the queue and wait for execution until the queue is emptied.
while(true) {
  queue = getNextQueue();
  task = queue.shift();
  execute(task);
  
+  while(microtaskQueue.hasTasks()) {
+    doMicrotask();
+  }
	  
  if(isRepaintTime()) repaint();
}
  • rAF queueBefore the start of each frame rendering pipeline flow, all queues are executed at one time rAF callback, and if generated new rAF, wait for the next frame to be executed.
while(true) {
  queue = getNextQueue();
  task = queue.shift();
  execute(task);
  
  while(microtaskQueue.hasTasks()) {
      doMicrotask();
  }
  
-  if(isRepaintTime()) repaint();
+  if(isRepaintTime()) {
+    animationTasks = animationQueue.copyTasks();
+    for(task in animationTasks) {
+      doAnimationTask(task);
+    }
+    
+    repaint();
+  }
}
  • Think, check if you understand
button.addEventListener('click', () => {
    
    
  Promise.resolve().then(() => console.log('Microtask 1'));
  console.log('Listener 1');
});

button.addEventListener('click', () => {
    
    
  Promise.resolve().then(() => console.log('Microtask 2'));
  console.log('Listener 2');
});

What is the sequence of clicking the buttons?

To analyze, the above code block is one task 0.

  1. task 0After the execution is complete, webapilisten to the event.
  2. When the user clicks the button, clickthe event , task queueenqueued task 1, task 2.
  3. Polling fetch task 1execution , Microtask queueenqueue Microtask 1.
    consoleoutput Listener 1. task 1Finished.
  4. Execute all microtask(currently only Microtask 1), take out the execution, and console output Microtask 1.
  5. Polling fetch task 2execution , Microtask queueenqueue Microtask 2.
    consoleoutput Listener 2. task 2Finished.
  6. Execute all microtask, fetch and Microtask 2execute , console output Microtask 2.

Answer: Listener 1-> Microtask 1-> -> Listener 2->Microtask 2

If you got the answer right, congratulations, the answerer 87%who .

answer

  • What if the code triggers it?
button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('Microtask 1'));
  console.log('Listener 1');
});

button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('Microtask 2'));
  console.log('Listener 2');
});

+ button.click();

same thinking analysis

  1. task 0Execute to button.click()Wait for the event callback to complete.
  2. Execute synchronously Listener 1and Microtask queueenqueue Microtask 1. consoleoutput Listener 1.
  3. Execute synchronously Listener 2and Microtask queueenqueue Microtask 2. consoleoutput Listener 2.
  4. clickfunction return, end task 0.
  5. Execute all microtask, fetch and Microtask 1execute , console output Microtask 1.
  6. Take out Microtask 2and execute , console output Microtask 2.

Answer: Listener 1-> Listener 2-> -> Microtask 1->Microtask 2

When doing automated testing, you need to be careful, sometimes it will produce results that are not the same as user interaction.

  • Finally, some difficult questions

Will the following code prevent athe link from jumping when the user clicks it?

const nextClick = new Promise(resolve => {
    
    
  link.addEventListener('click', resolve, {
    
     once: true });
});
nextClick.then(event => {
    
    
  event.preventDefault();
  // handle event
});

What if it's a code click?

link.click();

The answer is not announced yet, welcome to discuss in the comment area.

node

  1. No script parsing events (eg, parsing script in HTML)
  2. no user interaction events
  3. NorAF callback
  4. No rendering pipeline (rendering pipeline)

Node does not need to keep polling whether there are tasks, and it ends when all queues are cleared.

common task queuetask queue

  1. XHR requests、disk read or write queue(I/O)
  2. check queue (setImmediate)
  3. timer queue (setTimeout)

Common Microtasksmicrotask queue

  1. process.nextTick
  2. Promise

process.nextTickExecution takes precedence over Promise.

while(tasksAreWaiting()) {
    
    
  queue = getNextQueue();
  
  while(queue.hasTasks()) {
    
    
    task = queue.shift();
    execute(task);
    
    while(nextTickQueue.hasTasks()) {
    
    
      doNextTickTask();
    }
    
    while(promiseQueue.hasTasks()) {
    
    
      doPromiseTask();
    }
  }
}

web worker

  • Noscript tag
  • no user interaction
  • Can't operateDOM

similarnode

reference

  1. Further Adventures of the Event Loop - Erin Zimmer@JSConf EU 2018
  2. In The Loop - Jake Archibald@JSconf 2018
  3. Animation - event loop

Guess you like

Origin blog.csdn.net/guduyibeizi/article/details/104233801