What is asynchrony

The so-called “asynchrony” simply means that a task is divided into two parts, the first part is executed first, and then the other part is executed. When the first part has results, the second part is executed again. JavaScript uses asynchronous programming for two reasons. One is that JavaScript is single-threaded, and the other is to improve CPU utilization. While improving the CPU utilization, it also increases the difficulty of development, especially in the readability of the code.

console.log(1);

setTimeout(function () {
  console.log(2);
});

console.log(3);
Copy the code

callback

When we first started to handle asynchrony, we used callback functions

asyncFunction(function(value){
	// todo
})
Copy the code

This is fine for simple situations, but it is not enough for slightly more complex scenarios, such as when there is too much asynchronous nesting.

Callback pyramid

But when we have a large number of asynchronous operations that depend on the results of the previous asynchronous execution, then we have a callback pyramid that is hard to read

step1(function (value1) {
    step2(function(value2) {
        step3(function(value3) {
            step4(function(value4) {
                // Do something with value4
            });
        });
    });
});
Copy the code

Of course, to improve on this nested way of writing, we have several ways to name functions

function fun1 (params) {
  // todo
  asyncFunction(fun2);
}

function fun2 (params) {
  // todo
  asyncFunction(fun3)
}

function fun3 (params) {
  // todo
  asyncFunction(fun4)
}

function fun4 (params) {
  // todo
}

asyncFunction(fun1)
Copy the code

2 Writing method based on event message mechanism

eventbus.on("init".function(){
    operationA(function(err,result){
        eventbus.dispatch("ACompleted");
    });
});
 
eventbus.on("ACompleted".function(){
    operationB(function(err,result){
        eventbus.dispatch("BCompleted");
    });
});
 
eventbus.on("BCompleted".function(){
    operationC(function(err,result){
        eventbus.dispatch("CCompleted");
    });
});
 
eventbus.on("CCompleted".function(){
    // do something when all operation completed
});
Copy the code

You can also take advantage of modularity to make the code easy to read. All three of these approaches improve the readability of the code, but they don’t solve another problem: exception catching.

The error stack

function a () {
    b();
}

function b () {
    c();
}

function c () {
    d();
}

function d () {
    throw new Error('Something went wrong.');
}

a();
Copy the code

From the above figure we can see that there is a relatively clear error stack information, a calls B-B calls c-C calls D, in D throws an exception. This means that when a function is executed in JavaScript, it is first pushed onto the stack, and when it is finished, the stack is removed, the FILO structure. We can easily locate the wrong place from the error message.

function a() {
    b();
}

function b() {
    c(cb);
}

function c(callback) {
    setTimeout(callback, 0)}function cb() {
    throw new Error('Something went wrong.');
}

a();
Copy the code

From the figure above we can see that the only exception printed is an exception in a callback function in setTimeout, and the execution order is untraceable.

Exception handling

Exceptions in the callback function cannot be caught because they are asynchronous, so we can only catch them in the callback using a try catch, which is the part I commented out.

function a() {
    setTimeout(function () {
        // try{
            throw new Error('Something went wrong.');
        // } catch (e) {
        
        // }
        
    }, 0);
}

try {
    a();
} catch (e) {
    console.log('I caught an exception. I'm so happy.');
}
Copy the code

While a try catch can only catch synchronous errors, there are some good ways to handle errors in a callback, such as the error-first code style convention, which is widely used in Node.js.

function foo(cb) {
  setTimeout((a)= > {
    try {
      func();
      cb(null, params);
    } catch(error) { cb(error); }},0);
}

foo(function(error, value){
	if(error){
		// todo
	}
	// todo
});
Copy the code

But it’s also easy to get caught up in the devil’s pyramid.

Promise

Specification briefly

  • A PROMISE is an object or function that has a THEN method.
  • A promise has three states: “pending”, “rejected” and “resolved”. Once the state is determined, it cannot be changed. The state can only change from “pending” to “Rejected” or “resolved”.
  • The first callback of the THEN method is called on success, and the second on failure.
  • Promise instances have a THEN method that must return a new promise.

Basic usage

// The asynchronous operations are placed in the Promise constructor
const promise1 = new Promise((resolve) = > {
    setTimeout((a)= > {
        resolve('hello');
    }, 1000);
});

// The operation after the asynchronous result is obtained
promise1.then(value= > {
  console.log(value, 'world');
}, error =>{
  console.log(error, 'unhappy')});Copy the code

Asynchronous code, synchronous writing

asyncFun()
	.then(cb)
	.then(cb)
	.then(cb)
Copy the code

Writing in this chained way, Promise solves the problem of callbacks dealing with multiple asynchronous nesting and makes the code easier to read, although the essence is to use callbacks.

Exception handling

If there is an exception in the asynchronous callback function, it will not be caught because the callback function is executed asynchronously. Let’s see how Promise solves this problem.

asyncFun(1).then(function (value) {
    throw new Error('Something went wrong.');
}, function (value) {
    console.error(value);
}).then(function (value) {},function (result) {
  console.log('There is an error', result);
});
Copy the code

Promise’s then method automatically tries and catches the callback function.

Promise.prototype.then = function(cb) {
	try {
		cb()
	} catch (e) {
       // todo
       reject(e)
	}
}
Copy the code

An exception thrown by a THEN method is caught by the second argument (if any) of the next cascaded THEN method, so what if the last THEN also has an exception.

Promise.prototype.done = function (resolve, reject) {
    this.then(resolve, reject).catch(function (reason) {
        setTimeout((a)= > {
           throw reason;
        }, 0);
    });
};
Copy the code
 asyncFun(1).then(function (value) {
     throw new Error('Then resolve callback error');
 }).catch(function (error) {
     console.error(error);
     throw new Error('Catch callback error');
 }).done((reslove, reject) = > {});
Copy the code

We can add a done method. This method does not return a Promise object, so it is not connected after that. The done method eventually throws the exception globally, where it can be caught by the global exception handler or interrupt the thread. This is also a best practice strategy for Promise, but the done method is not implemented by ES6, so we had to implement it ourselves without using third party Promise open source libraries. Why this done method is needed.

const asyncFun = function (value) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(value);
    }, 0); })}; asyncFun(1).then(function (value) {
  throw new Error('Then resolve callback error');
});
Copy the code

(node:6312) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Then resolve: (node:6312) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code As you can see, the JavaScript thread is only sending a warning, but it is not aborting the thread. A serious error can cause damage if the thread is not aborted in time.

limited

One limitation of a Promise chain is that it cannot be aborted. For example, if an error occurs in a promise chain, it is no longer necessary to continue. But promises do not provide a native way to cancel. But the Promise chain doesn’t stop. Although we can terminate the promise chain by returning a Promise in the pending state.

const promise1 = new Promise((resolve) = > {
    setTimeout((a)= > {
        resolve('hello');
    }, 1000);
});

promise1.then((value) = > {
    throw new Error('Wrong! ');
}).then(value= > {
    console.log(value);
}, error=> {
    console.log(error.message);
    return result;
}).then(function () {
    console.log('DJL's xiao');
});
Copy the code

Special scenario

  • When one of our tasks depends on multiple asynchronous tasks, we can use Promise.all
  • When our task depends on one of several asynchronous tasks, it doesn’t matter which one, promise.race

The above mentioned promise implementations for ES6 are actually less functional and lacking, so there are many open source Promise libraries, such as Q.JS, that provide more syntax-sugar and more scenarios for adaptation.

The core code

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            value = _value;
            for (var i = 0, ii = pending.length; i < ii; i++) {
                var callback = pending[i];
                callback(value);
            }
            pending = undefined;
        },
        then: function (callback) {
            if (pending) {
                pending.push(callback);
            } else{ callback(value); }}}};Copy the code

When called then, all callbacks are queued. When called resolve, all callbacks are queued

var ref = function (value) {
    if (value && typeof value.then === "function")
        return value;
    return {
        then: function (callback) {
            returnref(callback(value)); }}; };Copy the code

This section of code to achieve cascading function, using recursion. If a promise is passed, the promise is returned directly, but if a value is passed, the value is wrapped as a promise.

generator

Basic usage

function * gen (x) {
    const y = yield x + 2;
    // console.log(y); // Guess what value will be printed
}

const g = gen(1);
console.log('first', g.next());  //first { value: 3, done: false }
console.log('second', g.next()); // second { value: undefined, done: true }
Copy the code

The common way to think about it is that the yield keyword surrenders the execution of the function, and the next method surrenders the execution of the function, and yield carries the results of the subsequent yield execution in the generator to the outside of the function, and the next method returns the outside data to the variable to the left of yield in the generator. This enables two-way flow of data.

Generator implements asynchronous programming

Let’s look at how generator implements an asynchronous program (*)

const fs = require('fs');

function * gen() {
    try {
        const file = yield fs.readFile;
        console.log(file.toString());
    } catch(e) {
        console.log('Exception caught', e); }}/ / actuator
const g = gen();

g.next().value('./config1.json'.function (error, value) {
  if (error) {
    g.throw('File does not exist');
  }
  g.next(value);
});
Copy the code

The parameters in next will be the results returned by the previous yield function. You can see that the code in the generator function feels synchronized, but the process of executing this seemingly synchronized code is complicated, i.e. process management. So we can borrow the co from TJ.

The generator with co

Here’s how to use it:

const fs = require('fs');
const utils = require('util');
const readFile = utils.promisify(fs.readFile);
const co = require('co');

function * gen(path) {
    try {
        const file = yield readFile('./basic.use1.js');
        console.log(file.toString());
    } catch(e) {
        console.log('Something went wrong.');
    }
}

co(gen());
Copy the code

As we have seen, using the co actuator with a generator and a promise can be very convenient, very similar to synchronous writing, and asynchronous errors can be easily tried and caught. The reason for using the utility function utils.promisify to turn a promise into a regular asynchronous function is that co may only yield a chunk, promise, generator, array, or object. One of the great benefits of using CO with a generator is that errors can be tried and caught.

async/await

Let’s take a look at the asynchronous writing of async/await

const fs = require('fs');
const utils = require('util');
const readFile = utils.promisify(fs.readFile);
async function readJsonFile() {
    try {
        const file = await readFile('.. /generator/config.json');
        console.log(file.toString());
    } catch (e) {
        console.log('Something went wrong.');
    }

}

readJsonFile();
Copy the code

We can see that async/await is written very much like a generator. In fact async/await is a syntax-sugar of a generator with an executor built in. And when an exception occurs during execution, the execution will be stopped. Of course, await must be followed by a promise, and the node version must be >=7.6.0. Of course, Babel can also be used in earlier versions.

supplement

In the process of development, we often have several projects at hand at the same time, so the node version requirements are likely to be different, so we need to install different versions of Node, and manage these different versions. NVM is recommended here, download NVM, install it, and use NVM List to view the node version list. Use the NVM Use version number to switch the version.

Catch the missing fish in Node.js

process.on('uncaughtException', (error: any) => {
    logger.error('uncaughtException', error)
})
Copy the code

Catch the fish in the net in the browser environment

window.addEventListener('onrejectionhandled', (event: any) => {
    console.error('onrejectionhandled', event)
})
Copy the code

Refer to the article

Promise Chinese mini book

Parse the Promise internal structure and step by step implement a complete Promise class that can pass all Test cases

Delve into Promise implementation details

DJL Xiao’s personal blog