References:

  • A simple explanation of processes and threads

  • The difference between concurrency and parallelism

  • Overview of Asynchronous Operations

  • Dig into the Promise implementation details

1. Basic concepts of computer

Before we get into JavaScript event loops and asynchronous programming, we need to understand some basic computer concepts.

(1) Von Neumann architecture

Hungarian-american mathematician Von Neumann proposed the stored Program Principle, or three basic principles of computer manufacturing, in 1946:

  • Binary numbers are used to represent data and instructions processed by computers

  • Sequential execution program

  • Computer hardware consists of input device, memory, controller, arithmetic unit and output device.

A central processing unit (CPU) is the control and computing center of a computer system. It is a collection of controllers and algorithms used to execute program instructions, or tasks.

With the continuous development of CPU, the current market is basically multi-core CPU, can perform multiple tasks at the same time, namely parallel.

(2) parallelism and concurrency

Parallelism and concurrency are two confusing concepts. Here is a distinction:

  • Parallelism: The ability to perform multiple tasks at the same time depending on multi-core cpus or multiple cpus.

  • Concurrency: A single single-core CPU can execute only one task at a time. By dividing tasks into time segments, the CPU can switch between different time segments to execute corresponding tasks, so that it can feel that multiple tasks can be executed in the same time segment at a macro level.

(3) Process, thread, coroutine

Multi-core cpus allow parallelism, which makes programs run faster, more efficiently, and with better performance.

Multithreading means that tasks can be processed in parallel, while threads cannot exist alone and need to be started and managed by processes.

A process is a running process of a program and the basic unit of resource allocation.

When a program is started, the operating system allocates a block of memory (for storing code and data) and a main thread (for performing tasks) to the program. Such a running process is called a process.

Thread is the linear execution process of a series of tasks and the basic unit of CPU scheduling.

There are a lot of tasks need to be executed in the process of running the program, we can choose to execute tasks in a single thread, or we can choose to execute tasks in parallel with multiple threads. During the process, the CPU is involved in scheduling, that is, to let the CPU to execute tasks.

Coroutines, also known as fibers or green threads, are not managed by the operating system but controlled by the program itself. They are lighter than threads and are used to improve performance and avoid the resource consumption of thread switching.

It simply means that a thread contains a set of tasks, and the coroutine breaks these tasks up and executes them.

2. Event loops

JavaScript is run single-threaded, meaning that only one task can be run at a time and other tasks must be queued. This is called the single-threaded model.

Although JavaScript runs on only one thread (called the main thread), the JavaScript engine itself supports multiple threads, and the single-threaded engine was originally designed to avoid complex browser scripts because of resource sharing and access conflicts between multiple threads.

Because JavaScript is single-threaded, it can easily block if a task takes too long to execute. In most cases, time-consuming I/O operations (input and output), such as data requests from the server, are performed. In this case, the CPU is idle. You can suspend the pending task and continue to execute the following tasks.

(1) Synchronous and asynchronous tasks

In JavaScript, tasks fall into two categories:

  • Synchronous task: A task queued to execute on the main thread.

  • Asynchronous task: task suspended by the engine and placed in a task queue.

First, the main thread will execute all synchronization tasks. After the synchronization task is completed, the main thread will check the asynchronous tasks that meet the execution conditions in the task queue and release the main thread to execute them as synchronization tasks. If other events (such as click) need to execute corresponding script tasks, or asynchronous tasks are generated during the execution of synchronous tasks, they are put into the corresponding task queue and continue to loop until the task queue is empty. This mechanism is called event loop.

(2) macro task, micro task

The event loop allows asynchronous tasks to be executed sequentially from the task queue, so there is a sequential order. What if there are asynchronous tasks with a higher priority (such as DOM update events)?

The answer is microtasks.

There are two types of asynchronous tasks in JavaScript:

  • Macrotask: Also called Task, initiated by the host environment (browser, Node).

  • Microtasks: Also known as Jobs, are initiated by JavaScript itself.

JavaScript puts asynchronous tasks into task queues, called macrotasks, each of which has a microtask queue associated with it. When the macro task is complete, the engine performs the micro task in the current macro task and then the next macro task.

Microtasks can occur in the following ways:

  • MutationObserver: Used to observe changes to DOM nodes

  • Then /catch/finally methods for the Promise instance: used to eliminate nested calls (callback hell) and multiple error handling

  • QueueMicrotask function: Used to add microtasks to the microtask queue

  • Process.nexttick on the Node.js server

Asynchronous tasks in other cases are macro tasks.

Note that if a new microtask is created during the execution of the microtask, it will be directly added to the current microtask queue to continue the execution.

(3) Delay queue

When the setTimeout timer is used to specify the number of milliseconds after the callback function is executed, if the callback function is directly put into the task queue, there is no guarantee that the callback function will be executed after the specified millisecond interval. In this case, the browser maintains a delay queue in addition to the task queue, which is used to store the tasks that need to be delayed.

So the order of execution of tasks is:

--> Execute synchronization task --> Execute macro task in task queue --> Execute macro task in task queue --> Execute macro task in delay queue --> Execute next macro task and start the loopCopy the code

Note that the delay queue is actually a hashMap data structure. When a task in the delay queue is executed, it will determine whether it is due and execute it when it is due.

3. Asynchronous programming

JavaScript enables asynchronous programming on a single thread through the mechanism of task queues and event loops. Here are some key concepts:

(1) Callback function

In JavaScript, functions can be passed as arguments, and such argument functions are called callback functions.

function fn1(){
    console.log('fn1');  
}
function fn2(fn){
    fn();
    console.log('fn2');
}
fn2(fn1);
// "fn1"
// "fn2"
Copy the code

We refer to the callbacks in synchronous tasks as synchronous callbacks and the callbacks in asynchronous tasks as asynchronous callbacks.

function callback(name){
    console.log(name);
}
setTimeout(callback,2000.'tom');
Copy the code

The advantage of the callback function is that it is simple, easy to understand, and easy to implement. The disadvantage is that the parts are highly coupled, the code structure is messy, and especially when nested calls, it is easy to create callback hell.

In addition to writing callback functions in code, asynchronous operations can also take the form of event listening and publish/subscribe, but are essentially performing callback functions.

(2) the timer

JavaScript provides the ability to execute code at regular times, called timers.

Timers are mainly implemented by setTimeout and setInterval functions to add scheduled tasks to the delay queue.

SetTimeout is used to specify how many milliseconds after a function or code is executed. You can cancel with the clearTimeout function by returning a unique timer number.

// Specifies that the code executes after 1000 milliseconds
setTimeout('console.log(1)'.1000);

// Specifies that the function executes after 1000 ms, and you can specify arguments to the callback function
setTimeout(function(name){
    console.log(name);
},1000.'tom')

// If milliseconds are not specified, milliseconds are 0
var timerId = setTimeout(function(){console.log('hello')});

// Cancel the timer
clearTimeout(timerId);
Copy the code

SetInterval is similar to setTimeout. It is used to execute a task at an interval of time, which is equivalent to an infinite number of scheduled executions. You can use the clearInterval function to cancel the timer.

// Execute every 1000 milliseconds
var timerId = setInterval(function(){
    console.log('hello');
},1000);

// Cancel the timer
clearInterval(timerId);
Copy the code

Note that the timing is sometimes inaccurate because asynchronous tasks generated by timers wait for synchronous tasks to finish executing.

(3) Anti-shake, throttling

Once timers are understood, they are often used to implement function debounce and function throttle.

Function debounce is used to prevent a function from being executed too often and only once if triggered multiple times.

How it works: The callback is executed n seconds after the event is triggered. If it is triggered again within n seconds, the timer is reset.

// Function stabilization by returning a new function with setTimeout and clearTimeout in it
function debounce(fn, delay){
    var timerId = null; // The timerId must be there, not in the returned function, otherwise the timerId will be initialized every time the function is executed
    return function(){
        var that = this;
        var args = Array.prototype.slice.call(arguments);
        if(timerId){
            clearTimeout(timerId);
            timerId = null;
        }
        timerId = setTimeout(function(){ fn.apply(that, args); },delay); }}// Drag to change the window size event triggered frequently, need to be shaken
function onResize(){
    console.log('resize');
}
window.addEventListener('resize',debounce(onResize,1000));
Copy the code

Function throttling is used to ensure that functions are executed only once per unit of time, so that they are executed evenly and infrequently.

// Function throttling is implemented by returning a new function in which to compare whether the timestamp is executed
function throttle(fn, gapTime){
    var lastTime = null;
    return function(){
        var that = this;
        var args = Array.prototype.slice.call(arguments);
        var nowTime = Date.now();
        if(! lastTime || nowTime - lastTime >= gapTime){ fn.apply(that, args); lastTime = nowTime; }}}// Function throttling implementation 2: by returning a new function, in the timer process
function throttle(fn, gapTime){
    var timerId = null;
    return function(){
        var that = this;
        var args = Array.prototype.slice.call(arguments);
        if(! timerId){ timerId =setTimeout(function(){
                fn.apply(that, args);
                timerId = null; }, gapTime); }}}// Prints every second
let fn = () = >{
  console.log('boom')}setInterval(throttle(fn,1000),10)
Copy the code

(4) Promise

Promise is a solution to asynchronous programming that is more reasonable and powerful than callback functions and events.

JavaScript provides Promise objects based on the ES6 standard, primarily to handle nested calls for asynchronous callbacks and error handling for merging multiple tasks.

Promise objects also have the disadvantage of not being able to cancel because the incoming function executes immediately; In addition, if the callback function is not specified, the internal error cannot be found.

The Promise object represents an asynchronous operation with three states:

  • Pending: Pending

  • This is a big pity

  • Rejected: It has failed

Only the result of an asynchronous operation can change this state, and it will never change again. This is how the Promise gets its name.

The initial state of a Promise object is pending and can change only in one of two ways:

  • Change the state from Pending to depressing by using the resolve function.

  • Change the state from Pending to Rejected using the Reject function.

Once the state changes, the subsequent addition of the callback function will immediately get the corresponding result.

Promise solves the nesting problem of asynchronous callbacks by delaying the binding of callback functions and by penetrating the return value of callback functions. Basic usage is as follows:

// Promise is also a constructor that generates a Promise instance
This function has resolve and reject arguments. It is a built-in function. The name can be changed, but the order cannot be changed.
A call to resolve or reject does not terminate the function. A call to resolve or reject does not affect the state
var pro = new Promise(function(resolve,reject){
    if(true) {// When the asynchronous operation succeeds, use the resolve function to send out the result, and the state changes from pending to depressing
        resolve('success');

        // Note that when the resolve argument is a Promise instance, the state of the current Promise instance is determined by the state of the Promise passed in
        resolve(new Promise(function(resolve2,reject2){
            reject2('2' failure); }}))else{
        Reject (reject); reject (reject); reject (reject)
        reject(new Error('failure'));

        // Reject is treated as a normal value when the reject argument is a Promise instance
        reject(new Promise(function(resolve2,reject2){
            resolve2('2' success); }}})));// After a Promise instance is generated, the callback function can be deferred through its then method
// Then method has two optional parameters, one is the callback function which is fulfilled when the state is fulfilled, and the other is the callback function which is fulfilled when the state is Rejected
The onResolved callback can receive the result of resolve
// The onRejected callback accepts errors from the reject function
function onResolved(value){
    console.log(value); 
}
function onRejected(error){
    console.log(error);
}
pro.then(onResolved, onRejected);

// The then method of a Promise instance always returns a new Promise instance
var pro = new Promise(function(resolve,reject){
    resolve('success');
});
If the then method does not specify a callback or the callback is not executed, the state of the new Promise instance is the same as the original Promise instance
var newPro = pro.then();
pro // This is a big pity. {< big pity >: "success "}
newPro // This is a big pity. {< big pity >: "success "}
newPro === pro // false
// If the then method specifies the callback function, and the callback function runs, the return value of the callback function will be the result of the new Promise instance, with the state of fulfilled
var newPro = pro.then(function(value){
    return 'New return value';
});
pro // This is a big pity. {< big pity >: "success "}
newPro // Promise {< big pity >: "new return value "}

// The catch method of the Promise instance is used to handle exceptions
// internal errors don't affect the outside, but Promise will always be delivered, "bubbling"
// Catch (function(){}) is an alias for then(null,function(){})
pro2.catch(function(error){});// -- equivalent to
pro2.then(null.function(error){});// The finally method of the Promise instance is used to perform the task that will always run
// Finally is executed when the onResolved and onRejected callback functions return or throw an error
The finally method returns a new Promise instance that is always in the same state as the original Promise instance, unless an error is reported
pro2.finally(function(){
    console.log('Always executing code');
    throw new Error('wrong');
})
Copy the code

Note: The reject outgoing error is the same as the throw error, and can be regarded as having the same effect.

In addition to the use of Promise instances, there are other methods built into Promise objects:

All () is used to wrap multiple Promise instances into a single Promise instance
// This is a big pity. When all the states are very depressing, the pro state will be a big pity
// Pro is rejected when there is a rejected state
var pro = Promise.all([pro1, pro2, pro3]);

// The following methods are used similarly to promise.all (), but with different state changes
var pro = Promise.race([pro1, pro2, pro3]); // When one of the states changes, the pro state changes
var pro = Promise.allSettle([pro1, pro2, pro3]); // When all the states change, the pro state will become depressing
var pro = Promise.any([pro1, pro2, pro3]); // This is a big pity. Pro is rejected when all states are rejected

The promise.resolve () method is used to convert arguments to produce a Promise instance
If the argument is a Promise instance, the result is returned as is; If it is an object with a THEN method, the then method of the object is called.
Promise.resolve(); // Promise {<fulfilled>: undefined}
Promise.resolve(1); // Promise {<fulfilled>: 1}
Promise.resolve(new Promise(function(resolve){resolve(1)})); // Promise {<fulfilled>: 1}
Promise.resolve({then:function(resolve){resolve(1)}}); // Promise {<fulfilled>: 1}
Promise.resolve({then:function(){}}); // Promise {<pending>}

The promise.reject () method is used to convert arguments to a Promise instance in the rejected state
Unlike the promise.resolve () method, promise.reject () returns an error regardless of the type of argument.
Promise.reject(new Promise(function(resolve){resolve(1)})); // Promise {<rejected>: Promise}
Promise.reject({then:function(){}}); // Promise {<rejected>: {then:function(){}}}

// promise.try () is used to make asynchronous callbacks to asynchronous code, synchronous callbacks to synchronous code, and catch errors, but is not supported by native JS.
var f = () = > console.log('now');
Promise.resolve().then(f);
console.log('next');
// "next"
// "now"
var f = () = > console.log('now');
Promise.try(f);
console.log('next');
// "now"
// "next"
Copy the code

New Promise(function(resolve, reject){});

(5) rewrite the Promise

The basic use of Promise and cover the method to understand, here try to write down the Promise, called MyPromise.

/** * rewrite Promise *@param Fn will immediately execute the function */
function MyPromise(fn){

    // (1) Basic data
    var that = this;
    that.state = 'pending'; / / state
    that.result = null; / / the result
    that.fulfilledCallbacks = []; // This is a big pity
    that.rejectedCallbacks = []; // The callback function in the rejected state

    // (2) resovle function: Change the state from pending to depressing, obtain the result and trigger the callback function
    function resolve(result){
        if(that.state ! = ='pending') return;

        // If the result is an object with a then method, its THEN method is called
        if(result === Object(result) && typeof result.then === 'function'){
            result.then(resolve,reject);
            return;
        }

        that.state = 'fulfilled';
        that.result = result;
        while(that.fulfilledCallbacks.length>0){
            that.fulfilledCallbacks.shift()(result); // Execute the callback function}}Reject (3) reject (reject); reject (reject); reject (reject); reject (reject); reject (reject); reject (reject
    function reject(result){
        if(that.state ! = ='pending') return;
        that.state = 'rejected';
        that.result = result;
        while(that.rejectedCallbacks.length>0){
            that.rejectedCallbacks.shift()(result); // Execute the callback function}}// (4) Execute the passed function, reject if there is an exception
    try{
        fn(resolve, reject);
    }catch(e){ reject(e); }}// Then method of MyPromise instance: used to bind the callback function (execute the callback directly if the state has changed) and return the new MyPromise instance
MyPromise.prototype.then = function(onFulfilled, onRejected){
    var that = this;
    onFulfilled = typeof onFulfilled === 'function'? onFulfilled : function(result){return result};
    onRejected = typeof onRejected === 'function'? onRejected : function(result){throw result};
    var myPromise = new MyPromise(function(resolve,reject){

        // The callback function for successful execution of the microtask
        var fulfilledCallback = function(){
            queueMicrotask(function(){
                try{
                    var result = onFulfilled(that.result);
                    resolvePromise(myPromise, result, resolve, reject);
                }catch(error){ reject(error); }}); }// A callback function when execution fails through the microtask
        var rejectedCallback = function(){
            queueMicrotask(function(){
                try{
                    var result = onRejected(that.result);
                    resolvePromise(myPromise, result, resolve, reject);
                }catch(error){ reject(error); }}); }if(that.state === 'pending') {/ / wait
            that.fulfilledCallbacks.push(fulfilledCallback);
            that.rejectedCallbacks.push(rejectedCallback);
        }else if(that.state === 'fulfilled') {/ / has been completed
            fulfilledCallback();
        }else if(that.state === 'rejected') {/ / has failedrejectedCallback(); }});return myPromise;
}

// the resolvePromise function is used to determine what to return
function resolvePromise(myPromise, result, resolve, reject){

    // The result is a newly returned instance of MyPromise, which results in a circular reference, so an error is reported
    if(result === myPromise){
        reject(new TypeError('Circular reference error'));
        return;
    }

    // If the result is an object with a THEN method, then its THEN method is called through the microtask, since the object may not have been built yet
    if(result === Object(result) && typeof result.then === 'function'){
        queueMicrotask(function(){
            var called = false; When both callback functions are called, the other result is ignored
            result.then(function(value){
                if(called) return;
                called = true;
                resolvePromise(myPromise,value,resolve,reject);
            },function(value){
                if(called) return;
                called = true;
                reject(value);
            });
        })
        return;
    }

    resolve(result);
}

/ / MyPromise. Resolve method
MyPromise.resolve = function(value){
    return new MyPromise(function(resolve){ resolve(value); })}Copy the code

Note that when the then method’s callback returns an object with a THEN method, its THEN method is executed by creating a microtask, since the object may not have been built yet.

If this object were an instance of MyPromise, an extra microtask would be created by calling the callback bound to its then method, which would result in a different order:

MyPromise.resolve().then(() = > {
    console.log(0);
    return MyPromise.resolve(4);
}).then((res) = > {
    console.log(res)
})

MyPromise.resolve().then(() = > {
    console.log(1);
}).then(() = > {
    console.log(2);
}).then(() = > {
    console.log(3);
}).then(() = > {
    console.log(5);
}).then(() = >{
    console.log(6);
})
/ / 0
/ / 1
/ / 2
/ / 3
/ / 4
/ / 5
/ / 6
Copy the code

Note that with the then method only the callback function is bound and stored, not put into the microtask queue, but put into the microtask when it needs to be executed.

(6) the Generator function

(7) async and await