Chapter 1 asynchrony: Present and future

1. Js is single threaded

The browser rendering process is multithreaded, as follows:

  • JS engine thread
  • Event trigger thread
  • Timing trigger thread
  • Asynchronous HTTP request threads
  • GUI rendering thread

Js is single-threaded because it prevents messy manipulation of the DOM. Single thread means only one task at a time. If there are multiple tasks, they need to be executed one by one. In order to solve asynchronous events, the JS engine produces the Event Loop mechanism.

1.1 EventLoop

Js engines don’t run on their own, they run in the host environment, which is usually the browser, but as nodeJ.s has moved into the server realm, JS has infiltrated other realms as well. Each of these host environments provides its own event loop.

So what is an event loop? Js is single-threaded, which means that only one task can be performed at a time. If there are multiple tasks, they need to be executed one by one. In order to solve asynchronous events, the JS engine generates the Event Loop mechanism. Js task execution will have a task queue, setTimeout is added to the end of the task queue after the set time. Therefore, it is a timer, but at the end of the set time, whether the callback function is executed depends on the state of the task queue. In layman’s terms, setTimeout is an “inaccurate” timer.

Until the ES6, js, essentially changed the event loop, where management ES6 precisely made the work details of event loop, one of the most important reason is that the introduction of Promise, this makes the event loop queue scheduling can run directly in the control, and not like the above said “not accurate” timer.

1. Macro tasks
  • In JS, most tasks are executed on the main thread. Common tasks include:
    • Render event
    • User interaction events
    • Js script Execution
    • Network requests, file read/write completion events, and so on.
    • SetTimeout and setInterval
  • In order for these events to work properly, the JS engine needs to arrange the order in which they are executed. V8 actually stores these tasks in a queue.
2. Microtasks

(1) For each macro task, there is an internal microtask

(2) Microtasks were introduced to solve the problem of asynchronous callbacks

  • The asynchronous callback is enqueued to the macro task queue.

With this approach, the callback should be executed after all the previous macro tasks have completed. If the current task queue is very long, the callback will not be executed, causing the application to stall.

  • Place the asynchronous callback at the end of the current macro task.

To get around this problem in the first approach, V8 introduces a second approach, the microtask approach. A microtask queue is defined in each macro task. When the macro task is completed, the microtask queue will be checked. If it is empty, the next macro task will be directly executed.

(3) Common microtasks include:

  • MutationObserver
  • Promise. Then (or. Reject) and with
  • Other technologies developed based on Promise (such as the FETCH API)
  • V8 garbage collection process.

Let’s look at a common interview question:

console.log('start'); 
setTimeout(() = > { 
  console.log('timeout'); 
}); 
Promise.resolve().then(() = > { 
  console.log('resolve'); 
}); 
console.log('end'); 
Copy the code
  • The task of synchronizing the queue is executed first, so start and end are printed first
  • SetTimeout is placed in the macro task queue as a macro task
  • Promise.then is put into the microtask queue as a microtask
  • Promise.resolve() changes the state of the Promise to successful, i.e. the macro task completes, checks the microtask queue, and finds a promise.then, executes
  • The next macro task, setTimeout, is executed

Here’s another example:

Promise.resolve().then(() = >{ 
  console.log('Promise1')   
  setTimeout(() = >{ 
    console.log('setTimeout2')},0)});setTimeout(() = >{ 
  console.log('setTimeout1') 
  Promise.resolve().then(() = >{ 
    console.log('Promise2')})},0); 
console.log('start'); 
 
// start 
// Promise1 
// setTimeout1 
// Promise2 
// setTimeout2 
Copy the code

Next from the history of JS asynchronous development to learn the relevant knowledge of asynchronous

Chapter two, callback function

Callbacks are the most basic asynchronous pattern in JS.

2.1 Callback hell

listen("click".function handle(evt){
	setTimeout(function request(){
		ajax("...".function response(test){
			if (text === "hello") {
				handle();
			} else{ request(); }})},500)})Copy the code

This code is often referred to as callback hell, or sometimes the destruction pyramid. It is difficult to understand, update, or maintain code that is strongly coupled to asynchronous operations, and whose upper and lower callbacks need to be modified whenever an operation needs to be modified.

2.2 Trust Issues

Some callbacks are not written by you or under your direct control. In most cases it is provided by a third party. In this inversion of control, I hand over part of my program’s execution control to a third party. There is no explicit contract between your code and a third-party tool. It creates a lot of messy logic and leads to a complete breakdown of the trust chain.

Chapter three, Promise

There are two drawbacks to the callback function: callback hell and a lack of credibility. Promise addressed both problems.

3.1 Meaning of Promise

A Promise is simply a container that holds the result of some event (usually an asynchronous operation) that will end in the future.

  • The state of the Promise object is unaffected. A Promise object represents an asynchronous operation that has three states: Pending, depressing, and Reject. Only the result of an asynchronous operation can determine which state is currently in, and no other operation can change that state.
  • Once the state changes it will never change again, and you can get this result at any time. There are two possibilities for the state change of a Promise: from Pending to depressing and from Pending to Rejected. Once the state changes, it doesn’t change, it stays the same.

A Promise is the equivalent of an order number for a meal. When we pay for the food we want, we get a receipt. You can watch a video or play a game while you wait for a delicious lunch in the back of the kitchen. When the waiter calls out our order, we can go to the front desk with the receipt to change our lunch. Sometimes, of course, the receptionist will tell you that the drumstick you ordered is out. That’s how Promise works.

3.2 Basic Usage

1, Promise

ES6 specifies that a Promise object is a constructor that generates a Promise instance.

var promise = new Promise(function(resolvem reject) {
	// some code
	if (/* Asynchronous operation succeeded */) {
		resolve(value);
	} else{ reject(error); }})Copy the code
  • Resolve changes the state of the Promise object from “incomplete” to “successful,” is called when the asynchronous operation succeeds, and the result of the asynchronous operation is passed as a parameter
  • Reject changes the state of the Promise object from “unfinished” to “failed,” called when an asynchronous operation fails, and passing any errors that the asynchronous operation throws up as arguments
Resolve, reject, and then() methods

After the Promise instance is generated, you can use the THEN method to specify the Resolved and Rejected state callback functions, respectively

promise.then(function(value) {
	// success
}, function(error) {
	// failure
})
Copy the code
  • The then method takes two arguments: the first callback is called when the Promise state changes to Resolved, and the second callback is called when the Promise state changes to Rejected

  • The second parameter is optional and does not have to be supplied

  • Both functions accept the value passed from the Promise object as an argument.

    • Reject an instance of the Error object passed halfway through the reject function, indicating that an Error was thrown.

    • The resolve function can pass a Promise instance in addition to the normal value

      var p1 = new Promise(function(resolve, reject) {
      	/ /...
      });
      
      In this case, the state of P1 determines the state of P2. P2 must wait until P1's state changes to resolve or reject before executing the callback function
      var p2 = new Promise(function(resolve, reject) {
      	/ /...
      	resolve(p1);
      });
      Copy the code

3.3 Promise. Prototype. Then ()

The then method is defined on the prototype object Promise.Prototype. It adds a callback to the Promise instance when it changes state.

  • The then method takes two arguments: the first callback is called when the Promise state changes to Resolved, and the second callback is called when the Promise state changes to Rejected

  • The then method returns a new Promise instance. So we can use the chain notation.

    promise((resolve, reject) = > {
    	// ...
    }).then(() = > {
    	// ...
    }).then(() = > {
    	// ...
    })
    Copy the code
  • The chaining notation specifies a set of callback functions that are called in order. If the previous callback returns a Promise instance, the later callback waits for the Promise object’s state to change before being called.

    promise((resolve, reject) = > {
    	// ...
    }).then(() = > {
    	// ...
    	return new Promise((resolve, reject) = > {
    		// ...
    	})
    }).then((comments) = > {
    	console.log("resolved: ", comments)
    }, (err) = > {
    	console.log("rejected: ", err)
    })
    
    // Or I could write it more succinctly
    promise((resolve, reject) = > {
    	// ...
    })
    .then(() = > new Promise((resolve, reject) = >{... }) .then(comments= > console.log("resolved: ", comments),
    	err= > console.log("rejected: ", err)
    )
    Copy the code

3.4 Promise. Prototype. The catch ()

Promise.prototype.catch() is an alias for method. Then (null, Rejection) that specifies the callback when an error occurs.

getJSON('/post.json').then((posts) = > {
	/ /...
}).catch((error) = > {
	console.log("Error occurred", error);
})
Copy the code
  • GetJSON returns a Promise object, and if it becomes Resolved, the then() method will be called

  • If an error occurs asynchronously or if the then method fails, it will be caught by a catch

  • A Promise throwing an error after the resolve statement is not caught, because once the Promise state changes, it never changes again.

    var promise = new Promise((resolve, reject) = > {
    	resolve('ok');
    	throw new Error('test')
    })
    promise
    	.then((value) = > {console.log(value)})
    	.catch((error) = > {console.log(error)})
    Copy the code
  • Errors on Promise objects have the “bubbling” nature of being passed backwards until they are caught. That is, errors are always caught by the next catch. In general, do not define a second function in then, but always use catch.

    var promise = new Promise((resolve, reject) = > {
    	resolve('ok');
    	throw new Error('test')})/ / do not recommend
    promise
    	.then(
    		(value) = > {console.log(value)},
    		(error) = > {console.log(error)}
    	)
    	
    / / recommend
    promise
    	.then((value) = > {console.log(value)})
    	.catch((error) = > {console.log(error)})
    Copy the code
  • Unlike traditional try/catch, if there is no callback that specifies error handling using catch, the error thrown by a Promise object is not passed to the outer code, that is, there is no response

  • A catch also returns a Promise object, followed by then

3.5 the done (), and finally ()

1. done()

Whether the callback chain of a Promise object ends in a then method or a catch method, if the last method throws an error, it may not be caught (because errors within a Promise do not bubble up globally). You can do this by providing a done() method, which is always at the end of the callback chain, ensuring that any errors that might occur are thrown.

asyncFunc ()
.then(f1)
.catch(f2)
.then(f3)
.done()
Copy the code

Its source code implementation is simple:

Promise.prototypr.done = function (onFulfilled, onRejected) {
	this.then(onFulfilled, onRejected)
	.catch(function(reason){
		// Throws a global error
		setTimeout(() = > {throw reason}, 0)})}Copy the code
2. finally()

The finally method is used to specify actions that will be performed regardless of what the Promise object ends up doing. The main difference with the done method is that it takes a callback function as an argument, which is executed anyway. Let’s see how it works.

Promise.prototype.finally = function (callback) {
	let P = this.constructor
    // This is a big pity. Resolve: this callback function will be implemented regardless of the Promise state (fulfilled or rejected)
	return this.then(
		value= > P.resolve(callback()).then(() = > value),
		reason= > P.resolve(callback()).then(() = > throw reason)
	)
}
Copy the code

3.6 Promise. All ()

The promise.all method is used to wrap multiple Promise instances into a new Promise instance

var p = Promise.all([p1, p2, p3])
Copy the code
  • P1, P2, and P3 are all Promise instances. If not, the promise. resolve method is used to convert the parameter to a Promise instance, and then processing

  • The arguments to this method do not have to be arrays, but must have an Iterator interface, and each member must be a Promise instance

  • The state of P is determined by P1, P2 and P3

    • Only when the states of P1, P2 and P3 become depressing, the state of P will become depressing. At this time, the return values of P1, P2 and P3 form an array and are passed to the callback function of P
    • If one of the states of P1, P2, and P3 changes to Rejected, p changes to Rejected, and the return value of the first Rejected instance is passed to p’s callback function
    var promises = [2.3.4.5.6.7].map((id) = > {
    	return getJSON(`/post/${id}.json`)})Promise.all(promises).then((posts) = > {
    	/ /...
    }).catch((error) = > {
    	/ /...
    })
    Copy the code
  • If a Promise instance that is a parameter defines a catch method itself, then it will not start the promise.all () catch method when it is rejected

const p1 = new Promise((resolve, reject) = > {
	resolve('hello')
})
.then(result= > result)
.catch(e= > e)

const p2 = new Promise(resolve, reject) => {
	throw new Error('error')
})
.then(result= > result)
.catch(e= > e)

const p3 = new Promise(resolve, reject) => {
	throw new Error('error')
})
.then(result= > result)

// P2's catch returns a new Promise instance whose final state is Resolved
Promise.all([p1, p2])
.then(result= > result)
.catch(e= > e)
// ["hello", Error: error]

// p3 has no catch of its own, so the error is caught by promise. all's catch
Promise.all([p1, p3])
.then(result= > result)
.catch(e= > e)
// Error: error
Copy the code

3.7 Promise. Race ()

The promise.race method is used to wrap multiple Promise instances into a new Promise instance

var p = Promise.race([p1, p2, p3])
Copy the code
  • P1, P2, and P3 are all Promise instances. If not, the promise. resolve method is used to convert the parameter to a Promise instance, and then processing

  • The arguments to this method do not have to be arrays, but must have an Iterator interface, and each member must be a Promise instance

  • The state of P is determined by P1, P2 and P3. As long as one instance of P1, P2 and P3 changes the state first, the state of P will change accordingly. The return value of the first instance to change state is passed to p’s callback function.

3.8 Promise. Resolve ()

The promise. resolve method converts an existing object into a Promise object in four cases:

1. The argument is an instance of Promise

Resolve will not make any changes

2. The parameter is a Thenable object

Thenable objects are objects that have then methods

let thenable = {
	then: function(resolve, reject) {
		resolve(42); }}let p1 = Promise.resolve(thenable)
p1.then(function(value) {
    console.log(value) / / 42
})
Copy the code

Promise. Resolve converts this object to a Promise object and then executes the thEnable object’s then method immediately

3. Arguments do not have then methods or are not objects at all

In this case, promise.resolve returns a new Promise object with the status Resolved

var p = Promise.resolve('hello');
p.then((s) = > {
	console.log(s)
})
// hello
Copy the code
4. No parameters

In this case, the promise.resolve method returns a Promise object in the Resolved state

console.log('start'); 
setTimeout(() = > { 
  console.log('timeout'); 
}); 
Promise.resolve().then(() = > { 
  console.log('resolve'); 
}); 
console.log('end'); 
Copy the code

(1) Perform the task of queue synchronization first, SetTimeout is put into the macro task queue as a macro task (3) promise.then is put into the micro task queue as a micro task (4) promise.resolve () changes the state of the Promise to successful, That is, after the macro task is executed, check the microtask queue and find a promise. then, execute (5). Then enter the next macro task — setTimeout, execute

3.9 Promise. Reject ()

The promise. reject method returns a new Promise instance with the state Rejected

Unlike promise.resolve, promise.reject passes its arguments as reject arguments to subsequent methods, so there is not as much case sorting

let thenable = {
	then: function(resolve, reject) {
		resolve(42); }}Promise.reject(thenable)
.catch(e= > {
	console.log(e === thenable)
})
//true
Copy the code

Chapter 4 Gnerator

Promise solves the problem of callback hell, but the biggest problem with Promise is the redundancy of the code. After the original task is wrapped by Promise, no matter what operation, at first sight, it is a pile of many THEN, and the original semantics become very unclear.

Traditional programming languages have long had solutions for asynchronous programming, one of which is called a coroutine, which means that multiple threads interact to accomplish asynchronous tasks. Its running flow is as follows:

  • Coroutine A starts execution
  • The execution of coroutine A is generally suspended, and execution is handed over to coroutine B
  • After some time, coroutine B returns execution
  • Coroutine A resumes execution
function *asyncJob () {
	// ...
	var f = yield readFile(fileA);
	// ...
}
Copy the code

The biggest advantage is that the code is written much like a synchronous operation.

4.1 Generator Encapsulates asynchronous Tasks

Generator functions are the largest implementation of coroutines in ES6 and feature the ability to hand over execution of functions.

The entire Generator function is a encapsulated container for asynchronous tasks, and asynchronous operations need to be specified with yield. Generator It can encapsulate asynchronous tasks for the following reasons:

  • Pause and resume execution
  • Data exchange inside and outside a function
  • Error handling mechanism

The syntactic aspects of the Generator functions in the above code were summarized in the previous blog post, so you can review them here if you don’t understand them.

A Generator function is a container for asynchronous operations. Its automatic execution requires a mechanism that automatically cedes execution rights when an asynchronous operation has a result. There are two ways to do this:

  • Callback functions: Wrap asynchronous operations as Thunk functions, in which execution rights are handed back

  • Promise objects: Wrap asynchronous operations as Promise objects and hand back execution rights using the then method

4.2 Thunk function

Parameter evaluation strategy has two kinds, one is called by value, the other is called by name

  • Pass calls, which evaluate arguments before they enter the function body; Performance may be compromised.
  • Called by name and evaluated when the argument is called.

The compiler’s implementation of the named call puts the arguments in a temporary function that is passed into the function body. This temporary function is called the Thunk function.

function f(m) {
	return m * 2;
}

f(x + 5);

/ / is equivalent to
var Thunk = function () {
	return x + 5;
}

function f(thunk) () {
	return thunk() * 2
}
Copy the code
1. Thunk function in js

Js is called by value, and its Thunk function has a slightly different meaning. In JS, the Thunk function replaces, instead of an expression, a multi-argument function, with a single-argument function that takes only a callback function as an argument.

(1) In JS, any function, as long as the argument has a callback function can be written in the form of Thunk function.

// ES5
var Thunk = function (fn) {
	return function () {
		var args = Array.prototype.slice.call(arguments);
		return function (callback) {
			return function (callback) {
				args.push(callback);
				return fn.apply(this, args)
			}
		}
	}
}

// ES6
var Thunk = function (fn) {
	return function (. args) {
		return function (callback) {
			return fn.call(this. args, callback) } } }/ / instance
function f (a, cb) {
    cb(a)
}
const ft = Thunk(f);
ft(1) (console.log); / / 1
Copy the code

(2) Use Thunkify module in production environment

$ npm install Thunkify

var thunkify = require('thunkify');
var fs = require('fs');

var read = thunkify(fs.readFile);
read('package.json') (function(err, str) {
	// ...
})
Copy the code
2. Process management of Generator functions

As mentioned earlier, Thunk can be used for automatic process management with Generator functions

(1) The Generator can be executed automatically

function *gen() {
	// ...
}

var g = gen();
var res = g.next();

while(! res.done) {console.log(res.value);
	res = g.next();
}
Copy the code

However, this is not suitable for asynchronous operations, and automatic execution above is not feasible if the previous execution must be completed before the next step can be performed.

(2) Thunk function automatically executed

var thunkify = require('thunkify');
var fs = require('fs');
var readFileThunk = thunkify(fs.readFile);

var gen = function* () {
	var r1 = yield readFileThunk('/etc/fstab');
	console.log(r1.toString());
	var r2 = yield readFileThunk('/etc/shell');
	console.log(r2.toString());
}
var g = gen();

// Pass the same function repeatedly to the value attribute of the next method
var r1 = g.next();
r1.value(function(err, data) {
	if (err) throw err;
	var r2 = g.next(data);
	r2.value(function (err, data) {
		if (err) throwerr; g.next(data); })})// the Thunk function automates process management
function run (fn) {
	var gen = fn();
	
	function next (err, data) {
		var result = gen.next(data);
		if (result.done) return;
		result.value(next)
	}
	
	next();
}

run(g)
Copy the code

The above run function is automatically executed as a Generator function. With this actuator, you can simply pass Generator functions to run no matter how many asynchronous operations there are inside, but note that each asynchronous operation is a Thunk function, which means yield must be followed by a Thunk function.

4.3 co module

The CO module does not need to write actuators for Generator functions

var co = require('co');
// The gen function executes automatically
co(gen);
// The co function returns a Promise object, so callbacks can be added using the then method
co(gen).then(function () {
    console.log('Generator completed execution ')})Copy the code
1. Automatic execution based on Promise objects
var fs = require('fs');

var readFile = function (fileName) {
	return new Promise(function (resolve, reject) {
		fs.readFile(fileName, function (error, data) {
			if (error) returnreject(error); resolve(data); })})}var gen = function* () {
	var r1 = yield readFileThunk('/etc/fstab');
	console.log(r1.toString());
	var r2 = yield readFileThunk('/etc/shell');
	console.log(r2.toString());
}
var g = gen()

// Manually add callbacks layer by layer using the then method
g.next().value.then(function(data){
	g.next(data).value.then(function(data){
		g.next(data)
	})
})

// Write an autoexecutor based on manual execution
function run (gen) {
    var g = gen();
    
    function next(data) {
        var result = g.next(data);
        if (result.done) return result.value;
        result.value.then(function (data) {
            next(data);
        })
    }
    
    next();
}

run(gen)
Copy the code

Chapter 5 async functions

The ES2017 standard introduces async functions to make asynchronous operations more convenient. Async functions are syntactic sugar for Generator functions.

An async function replaces the * of a Generator with async and yields with await.

varasyncReadFile = async function () {
	var r1 = await readFileThunk('/etc/fstab');
	console.log(r1.toString());
	var r2 = await readFileThunk('/etc/shell');
	console.log(r2.toString());
}
Copy the code

Async improves on Generator in three ways:

  • Built-in actuators: There is no need to introduce the Thunk function and co module as Generator functions do to solve the problem of automatic execution
  • More general applicability: In Generator functions yield only Thunk or Promise objects, in async functions Promise objects and primitives (numbers, strings, booleans, but this is equivalent to synchronous operations)
  • The return value is Promise: much more convenient than Generator functions that return an Iterator object
1. Declaration of async functions
// Function declaration
async function foo() {}

// Function expression
const foo = async function() {}

// Arrow function
const foo = async() = > {}// Object method
let obj = { async foo() {} }
obj.foo().then(...)

/ / a class method
class Storage {
	constructor () {... }async getName(){}}Copy the code
2. Grammar

(1) Async returns a Promise object

  • The value returned by the return statement inside the async function becomes an argument to the then method callback function
async function f() {
	return 'hello'
}

f().then(v= > console.log(v)) // hello
Copy the code
  • An error thrown inside async causes the returned Promise object to become reject, and the thrown error object is received by the catch method callback.
async function f() {
	 throw new Error('Wrong');
}

f().then(
	v= > console.log(v)
	e => console.log(e)
)
// Error: there is an Error
Copy the code
  • Promise objects returned by async functions must wait until all Promise objects following the internal await command have finished executing, unless a return statement or an error is thrown.

(2) await command

  • Normally an await command is followed by a Promise object, and resolve is immediately converted into a Promise object if it is not

  • If the Promise object after the await command becomes reject, the reject argument is received by the catch callback

  • Sometimes you don’t want to throw an error to terminate the next step

    • Put await in try… Inside the catch structure
    • Add a catch method after the Promise object after await
async function f() {
	try {
		await Promise.reject('Wrong')}catch(e) {
	}	
	return await Promise.resolve('hello')
}

f().then( v= > console.log(v)) // hello

async function f1() {
    await Promise.reject('Wrong')
		.catch(e= > console.log(e));
	return await Promise.resolve('hello')
}

f1().then( v= > console.log(v)) // hello
Copy the code
  • The await command can only be used in async functions, otherwise an error will be reported

  • If the asynchronous operations following the await command are not secondary relationships, it is best to let them fire synchronously

let foo = getFoo();
let bar = getBar();

/ / write 1
let [foo, bar] = await Promise.all([getFoo(), getBar()])

/ / write 2
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
Copy the code

References:

  • Idol god Sanyuan’s blog
  • Ruan Yifeng’s ES6
  • JavaScript you Don’t Know (Middle)