Related knowledge points

  • Threads in JS
  • EventLoop
  • Macro and micro tasks

Threads in JS

Why is JS single threaded?

The single threaded JS feature is that only one task can be executed at a time.

This is due to some interaction with the user and operations related to manipulating the DOM and so on, so JS uses a single thread, otherwise using multiple threads would introduce complex synchronization issues. If it’s multithreaded, and one thread is modifying the DOM and the other thread is deleting it, which one will prevail?

If the execution synchronization problem, multithreading needs to lock, the execution of the task is very tedious.

Note: Although the HTML5 standard allows JavaScript scripts to create multiple threads, child threads are completely controlled by the main thread and cannot manipulate the DOM.

Problems with single threads

Since JavaScript is single-threaded, single-threaded means blocking until one task is complete before the next one can be executed. This will lead to the page stuck state, the page does not respond, affecting the user experience, so have to appear synchronous task and asynchronous task solutions.

  • Synchronization task: A task that is queued to be executed on the main thread can be executed only after the previous task is completed.
  • Asynchronous task: a task that does not enter the main thread but enters the “task queue” will enter the main thread only when the “task queue” notifies the main thread that an asynchronous task is ready for execution.

How does JS implement asynchronous programming?

In order to solve the above problems, asynchronous task execution appears, and its operation mechanism is as follows:

  • All synchronization tasks are executed on the main thread, forming an execution stack;
  • In addition to the main thread, there is a “task queue”. Whenever an asynchronous task has a result, an event is placed in the “task queue”;
  • Once all synchronization tasks in the execution stack have been executed, the system reads the “task queue” to see what events are in it. Those corresponding asynchronous tasks then end the wait state, enter the execution stack, and start executing;
  • The main thread repeats the third part above;

Note: When the main thread reads the task queue, the task queue is a “first in, last out” data structure, and the first events are read by the main thread first. When the stack is empty, the first event of the task queue enters the main thread. At the same time, if there is a timer, the main thread needs to check the time when the timer is executed.

EventLoop – an EventLoop mechanism

Before diving into the event loop, a few concepts need to be understood:

  • Execution context (Execution context)
  • Execution stack (Execution stack)
  • Micro tasks (micro-task)
  • Macro task (macro-task)

Execution context

An execution context is an abstract concept that can be understood as an environment in which code is executed. JS execution context is divided into three types: global execution context, function execution context, Eval execution context.

  • Global execution context: This is the default or base context, and any code that is not inside a function is in the global context. It does two things: it creates a globalWindowObject (browser case), and setthisIs equal to the global object, which can be an externally loaded JS file or local<scripe></script>The code in the tag. There can be only one global execution context in a program;
  • Function execution context: Each time a function is called, a new context is created for that function. Each function has its own execution context, but is created when the function is called. There can be any number of function execution contexts;
  • EvalFunction execution context: executes inevalThe code inside a function also has its own execution context;

Execution stack

The execution stack is the “stack” in our data structure. It has the characteristic of “first in, last out”. It is because of this characteristic that when our code is executing, it encounters an execution context and pushes it into the execution stack successively.

When the code is executed, the code in the execution context at the top of the stack is executed first. When the execution context code at the top of the stack is completed, it is removed from the stack and continues to execute the next execution context at the top of the stack.

function foo() {
  console.log("a");
  bar();
  console.log("b");
}

function bar() {
  console.log("c");
}

foo();
Copy the code
  • Initialization state, execute stack task is empty;
  • fooFunction execution,fooEnter execution stack, outputa, hit the functionbar;
  • thenbarThen enter the execution stack and start executionbarFunction, outputc;
  • barAfter a function exits the stack, it continues to execute the function at the top of the stackfoo, and finally outputb;
  • fooOut of the stack, all tasks in the execution stack are completed;

Event loop mechanism

The illustration above explains:

  • Synchronous and asynchronous tasks enter different execution “places”, synchronous to the main thread, asynchronous to the main threadEvent TableAnd register the function;
  • When the assigned task is complete,Event TableI’m going to move this function inEvent Queue;
  • When the code in the stack completes execution, the stack (call stackWhen the task in) is empty, the task queue (Event quene) to execute the corresponding callback;
  • This loop forms the JS event loop mechanism (Event Loop);

Macro and micro tasks

Let’s look at the result of this code:

console.log("script start");

setTimeout(function () {
  console.log("setTimeout");
}, 500);

Promise.resolve()
  .then(function () {
    console.log("promise1");
  })
  .then(function () {
    console.log("promise2");
  });

console.log("script end");
Copy the code

The output is:

script start
script end
promise1
promise2
setTimeout
Copy the code

JS has two types of tasks: macrotask (macrotask) and microtask (microtask). In ECMAScript, microtask is called jobs, and macrotask can be called task.

Macro task (macrotask )

The code executed by each execution stack is a macro task (including fetching an event callback from the event queue and placing it on the execution stack each time);

  • Each macro task completes the task from start to finish and does nothing else;
  • Browser to enable JS internal macro tasks withDOMTasks can be executed in an orderly manner, the page will be re-rendered after one macro task is executed and before the next macro task is executed (macro task => Render => macro task =>…). ;

Micro tasks (microtask )

A task that can be understood to be executed immediately after the execution of the current macro task;

  • That is, after the current macro task task, before the next macro task, before rendering;
  • So its response speed compared tosetTimeout(because thesetTimeoutIs a macro task) is faster because there is no need to wait for rendering;
  • That is, after a macro task is executed, all microtasks generated during its execution are executed (before rendering);

What kind of scenarios will form macro and micro tasks?

  • macrotask:
    • Main code block;
    • setTimeout;
    • setIntervalEtc. (as you can see, each event in the event queue is a macro task);
  • microtask:
    • process.nextTick;
    • Promise.then;
    • catch;
    • finallyAnd so on;

Note: In a Node environment, process.nextTick takes precedence over Promise, which means that the nextTickQueue portion of the microtask will be executed after the macro task ends, and then the Promise portion of the microtask will be executed.

Operation mechanism

Now that that’s clear, how does the event loop work? The sequence of tasks described below is based on the function call stack.

  • First, the event loop mechanism is subordinatescriptThe code inside the tag starts, as we mentioned above, with the wholescriptThe tag is handled as a macro task;
  • During code execution, if you encounter a macro task such as:setTimeout, the current task will be distributed to the corresponding execution queue;
  • In the process of execution, if micro-tasks are encountered, such as:PomiseIn creatingPromiseInstance object, the code executes sequentially, if executedThen,Operation, the task will be distributed to the microtask queue;
  • scriptThe code in the tag is executed, and the macro tasks and micro tasks involved in the execution process are also allocated to the corresponding queue;
  • At this point, the macro task is completed, and then the microtask queue to execute all the existing microtasks;
  • After the execution of the micro task, the first round of message cycle is completed, and the page is rendered once;
  • Then the second message loop begins, fetching tasks from the macro task queue for execution;
  • If there are no more tasks in the two task queues, all tasks are completed.

Macro and micro tasks

The above also briefly introduced the concept of macro task and micro task, below we mainly through the code to explain, a little bit more examples, please be patient to finish reading ~~~

Subject to a

console.log("1");

setTimeout(() = > {
  console.log("2");
}, 1000);

new Promise((resolve, reject) = > {
  console.log("3");
  resolve();
  console.log("4");
}).then(() = > {
  console.log("5");
});

console.log("6");
Copy the code

The output is:

1
3
4
6
5
2
Copy the code
  • Initialization state, execution stack is empty;
  • Executed first<script>Tag synchronization code, at this time the global code into the execution stack, synchronous sequential execution of the code, output 1;
  • Asynchronous code encountered during executionsetTimeout(macro task), which is assigned to the macro task asynchronous queue;
  • The synchronization code continues and one is encounteredpromiseAsynchronous code (microtask), but the code in the constructor is synchronous code, output 3, 4 in sequence, thenthenSubsequent tasks are added to the microtask queue;
  • Finally, execute the synchronization code, output 6;
  • becausescriptThe code inside is processed as a macro task, so this loop will process all asynchronous tasks in the microtask queue until all tasks in the microtask queue are executed. There is only one microtask in the microtask queue, so the output is 5.
  • At this time the page to a page rendering, rendering completed, for the next cycle;
  • Retrieves a macro task from the macro task queue, the previous onesetTimeout, finally output 2;
  • At this point, the task queue is empty, the execution stack is empty, and the whole program is executed.

Add macro tasks and micro tasks to the main thread

Execution order: Main thread => Micro tasks created on main thread => macro tasks created on main thread

console.log("start");

setTimeout(() = > {
  console.log("setTimeout"); // Put the callback code into another macro task queue
}, 0);

new Promise((resolve, reject) = > {
  for (let i = 0; i < 5; i++) {
    console.log(i);
  }
  resolve();
}).then(() = > {
  console.log("Promise instance callback executes successfully"); // Put the callback code into the microtask queue
});

console.log("end");
Copy the code

The output is:

start
0
1
2
3
4
5
end
PromiseThe instance callback successfully executedsetTimeout
Copy the code

Create a microtask in a microtask

Execution order: Main thread => Microtasks created on the main thread => microtasks created on the microtask => macro tasks created on the main thread

If a microtask is included in a microtask, it will be executed first, or before the macro task created on the main thread.

setTimeout(_= > console.log(4)); / / macro task

new Promise(resolve= > {
  resolve();
  console.log(1); / / synchronize
}).then(_= > {
  console.log(3); Micro / / task
  Promise.resolve()
    .then(_= > {
      console.log("before timeout"); // Create a microtask in the microtask, which is executed before the macro task
    })
    .then(_= > {
      Promise.resolve().then(_= > {
        console.log("also before timeout");
      });
    });
});

console.log(2);
Copy the code

The output is:

1
2
3
before timeout
also before timeout
4
Copy the code

Two microtasks are performed simultaneously:

setTimeout(_= > console.log(5)); / / macro task

new Promise(resolve= > {
  resolve();
  console.log(1); / / synchronize
})
  .then(_= > {
    console.log(3); Micro / / task
    Promise.resolve()
      .then(_= > {
        console.log("before timeout"); // Create a microtask in the microtask, which is executed before the macro task
      })
      .then(_= > {
        Promise.resolve().then(_= > {
          console.log("also before timeout");
        });
      });
  })
  .then(_= > {
    console.log(4); Micro / / task
  });

console.log(2);
Copy the code

The output is:

1
2
3
before timeout
4
also before timeout
5
Copy the code
setTimeout(_= > console.log(5)); / / macro task

new Promise(resolve= > {
  resolve();
  console.log(1); / / synchronize
})
  .then(_= > {
    console.log(3); Micro / / task
    Promise.resolve()
      .then(_= > {
        console.log("before timeout1"); // Create a microtask in the microtask, which is executed before the macro task
      })
      .then(_= > {
        Promise.resolve().then(_= > {
          console.log("also before timeout1");
        });
      });
  })
  .then(_= > {
    console.log(4); Micro / / task
    Promise.resolve()
      .then(_= > {
        console.log("before timeout2"); // Create a microtask in the microtask, which is executed before the macro task
      })
      .then(_= > {
        Promise.resolve().then(_= > {
          console.log("also before timeout2");
        });
      });
  });

console.log(2);
Copy the code

The output is:

1
2
3
before timeout1
4
before timeout2
also before timeout1
also before timeout2
Copy the code

Create a microtask in a macro task

Execution order: Main thread => Macro task queue on the main thread => micro tasks created in the macro task queue

Create a microtask in a macro task, and after the current macro task is executed, the microtask contained in the macro task is executed.

setTimeout(() = > {
  console.log("timer_1");
  setTimeout(() = > {
    console.log("timer_3");
  }, 500);
  new Promise(resolve= > {
    resolve();
    console.log("new promise");
  }).then(() = > {
    console.log("promise then");
  });
}, 500);

setTimeout(() = > {
  console.log("timer_2");
}, 500);

console.log("end");
Copy the code

The output is:

end
timer_1
new promise
promise then
timer_2
timer_3
Copy the code

Macro tasks created in microtasks

Execution order: Main thread => Micro tasks created on the main thread => macro tasks created on the main thread => macro tasks created on the micro task

There is only one asynchronous macro task queue. When a macro task is created in a micro task, it is appended to the asynchronous macro task queue (the same queue as the main thread).

new Promise(resolve= > {
  console.log("new Promise(macro task 1)");
  resolve();
}).then(() = > {
  console.log("micro task 1");
  setTimeout(() = > {
    console.log("macro task 3");
  }, 500);
});

setTimeout(() = > {
  console.log("macro task 2");
}, 1000);

console.log("Task queue");
Copy the code

The output is:

new Promise(macro task 1)
Task queue
micro task 1
macro task 3
macro task 2
Copy the code
new Promise(resolve= > {
  console.log("new Promise(macro task 1)");
  resolve();
}).then(() = > {
  console.log("micro task 1");
  setTimeout(() = > {
    console.log("macro task 3");
  }, 1000);
});

setTimeout(() = > {
  console.log("macro task 2");
}, 1000);

console.log("Task queue");
Copy the code

The output is:

new Promise(macro task 1)
Task queue
micro task 1
macro task 2
macro task 3
Copy the code

SetTimeout (() => {// macro task 2 console.log(‘macro task 2’); }, 1000) setTimeout(() => {// macro task 2 console.log(‘macro task 2’); }, 500) it will be executed before Macro Task 3 because the timer will be added to the event queue in milliseconds.

Comprehensive examples:

console.log("======== main task start ========");

new Promise(resolve= > {
  console.log("create micro task 1");
  resolve();
}).then(() = > {
  console.log("micro task 1 callback");
  setTimeout(() = > {
    console.log("macro task 3 callback");
  }, 0);
});

console.log("create macro task 2");

setTimeout(() = > {
  console.log("macro task 2 callback");
  new Promise(resolve= > {
    console.log("create micro task 3");
    resolve();
  }).then(() = > {
    console.log("micro task 3 callback");
  });
  console.log("create macro task 4");
  setTimeout(() = > {
    console.log("macro task 4 callback");
  }, 0);
}, 0);

new Promise(resolve= > {
  console.log("create micro task 2");
  resolve();
}).then(() = > {
  console.log("micro task 2 callback");
});

console.log("======== main task end ========");
Copy the code

The output is:

======== main task start ========
create micro task 1
create macro task 2
create micro task 2
======== main task end ========
micro task 1 callback
micro task 2 callback
macro task 2 callback
create micro task 3
create macro task 4
micro task 3 callback
macro task 3 callback
macro task 4 callback
Copy the code

Contains the async/await

Subject to a

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end"); Joins the microtask queue after the await statement
}
async function async2() {
  console.log("async2");
}

async1();

new Promise(resolve= > {
  console.log("create micro task");
  resolve();
}).then(() = > {
  console.log("micro task callback");
});

console.log("script start");
Copy the code

The output is:

async1 start
async2
create micro task
script start
async1 end
micro task callback
Copy the code

Topic 2

async function job1() {
  console.log("a");
  await job2();
  console.log("b"); // Add to the microtask queue
}

async function job2() {
  console.log("c");
}

setTimeout(function () {
  new Promise(function (resolve) {
    console.log("d");
    resolve();
  }).then(function () {
    console.log("e");
  });
  console.log("f");
});

job1();

new Promise(function (resolve) {
  resolve();
}).then(function () {
  console.log("g");
});

console.log("h");
Copy the code

The output is:

a
c
h
b
g
d 
f
e
Copy the code

The title three

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}

async function async2() {
  console.log("async2");
}

console.log("script start");

setTimeout(function () {
  console.log("setTimeout");
}, 0);

async1();

new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("promise2");
});

console.log("script end");
Copy the code

The output is:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
Copy the code

The title four

async function t1() {
  console.log(1);
  console.log(2);
  new Promise(function (resolve) {
    console.log("promise3");
    resolve();
  }).then(function () {
    console.log("promise4"); // Microtask 1
  });
  await new Promise(function (resolve) {
    console.log("b");
    resolve();
  }).then(function () {
    console.log("t1p"); // Microtask 2
  });

  Add 1 to the microtask queue after the await statement
  // Microtask 5
  console.log(3);
  console.log(4);

  new Promise(function (resolve) {
    console.log("promise5");
    resolve();
  }).then(function () {
    console.log("promise6");
  });
}

setTimeout(function () {
  console.log("setTimeout");
}, 0);

async function t2() {
  console.log(5);
  console.log(6);
  await Promise.resolve().then(() = > console.log("t2p")); // Microtask 4

  Join the microtask queue 2 after the await statement
  // Microtask 6
  console.log(7);
  console.log(8);
}

t1();

new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("promise2"); // Microtask 3
});

t2();

console.log("end");
Copy the code

The output is:

1
2
promise3
b
promise1
5
6
end
promise4
t1p
promise2
t2p
3
4
promise5
7
8
promise6
setTimeout
Copy the code

The code following the await statement must wait until the await statement has been executed (including the micro-task) before it can execute the subsequent code. In other words, all the code following the await statement will be added to the micro-task only after the await statement has been executed. Therefore, when an await promise is encountered, You must wait for the await promise function to complete before adding all the code following the await statement to the microtask.

So, while waiting to await promise.then microtask:

  • Run other synchronization code;
  • Wait until the synchronized code runs, and start runningawait promise.thenMicro tasks;
  • await promise.thenWhen the micro task is complete, put theawaitAll code following the statement is added to the microtask column;

Await statements are synchronous and all the code following await statements is an asynchronous microtask.

Topic five

async function b() {
  console.log("b");
  await c();
  // Join microtask queue 2
  console.log("b1");
}

async function c() {
  console.log("c");
  await new Promise(function (resolve, reject) {
    console.log("promise1");
    resolve();
  }).then(() = > {
    console.log("promise1-1"); // Microtask 2
  });

  // Join microtask queue 1
  setTimeout(() = > {
    console.log("settimeout1");
  });

  console.log("c1");
}

new Promise(function (resolve, reject) {
  console.log("promise2");
  resolve();
  console.log("promise2-1");
  reject();
})
  .then(() = > {
    console.log("promise2-2"); // Microtask 1

    setTimeout(() = > {
      console.log("settimeout2");
      new Promise(function (resolve, reject) {
        resolve();
      }).then(function () {
        console.log("settimeout2-promise");
      });
    });
  })
  .catch(() = > {
    console.log("promise-reject");
  });

b();

console.log("200");
Copy the code

The output is:

promise2
promise2-1
b
c
promise1
200
promise2-2
promise1-1
c1
b1
settimeout2
settimeout2-promise
settimeout1
Copy the code

Execution steps:

  • Executing from the top down,new PromiseFunction immediately executed => Print:promise2
  • resolve()promise2-2In themicroTask queue
  • settimeout2In theThe macroTask queue
  • Proceed further => Print:promise2-1
  • performb()Function => Print:b
  • performawait c()Function => Print:c
  • Enter theawait c()In the functionnew PromiseFunction => Print:promise1
  • resolve()promise1-1In themicroTask queue
  • settimeout1In theThe macroTask queue
  • If no, go to Print:200
  • I’m not on a mission. GomicroPerform the first entry in the missionmicroAction to be performed => Print:promise2-2
  • Continue to performmicroActions to be performed in the sequence of tasks => Print:promise1-1
  • microIf no action is performed, continue the taskc()The code to execute in the function => prints:c1
  • c()After the command is executed, continueb()The code to be executed in the function => print:b1
  • At this point, execute immediatelymicroNo further action is requiredThe macroThe first task to be executed in the task queue => Print:settimeout2
  • The macroIn task one, executenew Promisefunctionresolve()settimeout2-promiseIn themicroTask in progress, execute immediatelymicroTask => Print:settimeout2-promise
  • Continue to performThe macroAction to be performed in a task => Print:settimeout1

Conclusion:

  • Microtask queue takes precedence over macro task queue.
  • Macro tasks created on the microtask queue are added to the end of the current macro task queue.
  • The microtasks created in the microtask queue are added to the end of the microtask queue.
  • As long as there are tasks in the microtask queue, the macro task queue will only wait for the completion of the microtask queue.
  • Only after it’s runawaitStatement, before theawaitAll code following the statement is added to the microtask column;
  • In case ofawait promise, must waitawait promiseOnly after the function is executedawaitAll the code following the statement is added to the microtask;
    • Waiting for theawait Promise.thenMicrotask:
      • Run other synchronization code;
      • Wait until the synchronized code runs, and start runningawait promise.thenMicro tasks;
      • await promise.thenWhen the micro task is complete, put theawaitAll code following the statement is added to the microtask column;