Detailed explanation of promise and generator in ES6

Time:2021-5-13

brief introduction

In addition to the new syntax features and some new APIs mentioned in the last article, there are two very important new features in ES6, promise and generator. Today we will explain these two new features in detail.

Promise

What is promise

Promise is a solution of asynchronous programming, which is more reasonable and powerful than the traditional solution “callback function and event”.

The so-called promise is simply a container that holds the result of an event (usually an asynchronous operation) that will end in the future.

Syntactically, promise is an object from which messages of asynchronous operations can be obtained.

Characteristics of promise

Promise has two characteristics

  1. The state of the object is not affected by the outside world.

The project object represents an asynchronous operation with three states: pending, resolved, and rejected.

Only the result of asynchronous operation can determine the current state. No other operation can change this state.

  1. Once the state changes, it won’t change again. You can get this result at any time.

There are only two possibilities for a project object to change its state: from pending to resolved and from pending to rejected.

This is totally different from the event. The characteristic of the event is that if you miss it and listen to it again, you will not get the result.

Advantages of promise

Promise expresses asynchronous operation as synchronous operation process, avoiding nested callback functions.

Promise object provides a unified interface, which makes it easier to control asynchronous operation.

Disadvantages of promise

  1. Promise cannot be cancelled. Once it is created, it will be executed immediately and cannot be cancelled halfway.
  2. If the callback function is not set, the error thrown by promise will not be reflected to the outside.
  3. When you are in the pending state, you can’t know which stage (just started or about to finish) you are going to.

Usage of promise

The promise object is a constructor used to generate promise instances:

var promise = new Promise(function(resolve, reject) { 
// ... some code 
If (/ * asynchronous operation succeeded * /){ 
resolve(value); 
} else { reject(error); } 
}
);

Project can be connected to then operation, and then operation can be connected to two function parameters. The parameter of the first function is the value of resolve when building project, and the parameter of the second function is the error of reject when building project.

promise.then(function(value) { 
// success 
}, function(error) { 
// failure }
);

Let’s take a concrete example

function timeout(ms){
    return new Promise(((resolve, reject) => {
        setTimeout(resolve,ms,'done');
    }))
}

timeout(100).then(value => console.log(value));

In Promise, a setTimeout method is invoked, and the resolve method is triggered at regular intervals, and the parameter done is passed.

Finally, the program outputs done.

The execution order of promise

Promise is executed as soon as it is created. However, the method in promise. Then will be called again after a call cycle. Let’s take a look at the following example:

let promise = new Promise(((resolve, reject) => {
    console.log('Step1');
    resolve();
}));

promise.then(() => {
    console.log('Step3');
});

console.log('Step2');

Output:
Step1
Step2
Step3

Promise.prototype.then()

The then method returns a new promise instance (note, not the original promise instance). Therefore, we can use the chain writing method, that is, the then method is followed by another then method

getJSON("/users.json").then(function(json){
    return json.name;
}).then(function(name){
    console.log(name);
});

The above code uses the then method to specify two callback functions in turn. After the completion of the first callback function, the returned result will be passed into the second callback function as a parameter

Promise.prototype.catch()

The project. Prototype. Catch method is an alias of. Then (null, rejection), which is used to specify the callback function when an error occurs.

getJSON("/users.json").then(function(json){
    return json.name;
}).catch(function(error){
    console.log(error);
});

The error of the promise object is “bubbling” and is passed back until it is caught. In other words, errors are always caught by the next catch statement

getJSON("/users.json").then(function(json){
    return json.name;
}).then(function(name){
    console.log(name);
}).catch(function(error){
    //Handle all previous errors
    console.log(error);
});

Promise.all()

The promise. All method is used to package multiple promise instances into a new promise instance

var p = Promise.all([p1,p2,p3]);
  1. Only when the states of P1, P2 and P3 become full, the state of P will become full. At this time, the return values of P1, P2 and P3 form an array and are passed to the callback function of P.
  2. As long as one of P1, P2 and P3 is rejected, the state of P becomes rejected. At this time, the return value of the first instance to be rejected will be passed to the callback function of P.

Promise.race()

The promise. Race method also packages multiple promise instances into a new promise instance

var p = Promise.race([p1,p2,p3]);

As long as one of the instances P1, P2 and P3 changes the state first, the state of P changes with it. The return value of the promise instance changed first is passed to the callback function of P

Promise.resolve()

Promise. Resolve() turns an existing object into a promise object

Promise.resolve('js');
//Equivalent to
new Promise(resolve => resolve('js'));

So what kind of object can be transformed into a promise object?

  1. Parameter is an instance of promise
  2. The parameter is a thenable object
  3. The parameter is not an object with the then method, or it is not an object at all
  4. Without any parameters

Promise.reject()

The project. Reject (reason) method also returns a new project instance with the status rejected

var p = Promise.reject('error');
//Equivalent to
var p = new Promise((resolve,reject) => reject('error'));

The parameters of the project. Reject () method will be used as the original reason for reject and become the parameters of subsequent methods. This is not consistent with the promise. Resolve method

done()

Whether the callback chain of promise object ends with then method or catch method, if the last method throws an error, it may not be able to catch it (because the internal error of promise will not bubble to the global). Therefore, we can provide a done method, always at the end of the callback chain, to ensure that any possible errors are thrown

asyncFunc().then(f1).catch(f2).then(f3).done();

finally()

The finally method is used to specify the operation that will be performed regardless of the final state of the promise object. The biggest difference between this method and the done method is that it takes an ordinary callback function as a parameter, which must be executed anyway

server.listen(1000).then(function(){
    //do something
}.finally(server.stop);

Generator

What is generator

Generator function is an asynchronous programming solution provided by ES6

Grammatically, it can be understood that the generator function is a state machine that encapsulates multiple internal states

Executing the generator function returns a traverser object

Formally, the generator function is a normal function, but it has two characteristics. First, there is an asterisk between the function keyword and the function name; Second, the yield statement is used inside the function body to define different internal states.

for instance:

function * helloWorldGenerator(){
    yield 'hello';
    yield 'world';
    return 'ending';
}

var gen = helloWorldGenerator();

Output results:

console.log(gen.next());
console.log(gen.next());
console.log(gen.next());

{ value: 'hello', done: false }
{ value: 'world', done: false }
{ value: 'ending', done: true }

yield

The running logic of the next method of the traverser object is as follows:

(1) When a yield statement is encountered, the subsequent operation is suspended, and the value of the expression immediately following yield is taken as the value attribute value of the returned object.

(2) The next time the next method is called, the execution continues until the next yield statement is encountered.

(3) If there is no new yield statement, it runs until the end of the function until the return statement, and takes the value of the expression after the return statement as the value attribute value of the returned object.

(4) If the function does not have a return statement, the value attribute value of the returned object is undefined.

Note that the yield sentence itself does not return a value, or always returns undefined.

The next method can take a parameter, which will be used as the return value of the previous yield statement.

function * f() {
    for( let i =0; true; i++){
        let reset = yield i;
        if(reset){
            i = -1;
        }
    }
}

let g = f();
console.log(g.next());
console.log(g.next());
console.log(g.next(true));

Output results:

{ value: 0, done: false }
{ value: 1, done: false }
{ value: 0, done: false }

As you can see in the last step, we use the true passed in next to replace the value of I, resulting in I = – 1 + 1 = 0

Let’s take another example

function * f2(x){
    var y = 2 * ( yield ( x + 1));
    var z = yield (y / 3);
    return (x + y + z);
}

var r1= f2(5);
console.log(r1.next());
console.log(r1.next());
console.log(r1.next());

var r2= f2(5);
console.log(r2.next());
console.log(r2.next(12));
console.log(r2.next(13));

Output results:

{ value: 6, done: false }
{ value: NaN, done: false }
{ value: NaN, done: true }

{ value: 6, done: false }
{ value: 8, done: false }
{ value: 42, done: true }

If next does not pass a value, yield itself does not return a value, so we will get Nan.

However, if next passes in a specific value, it will replace the yield and become the real return value.

yield *

If you call another generator function inside the generator function, it has no effect by default

function * a1(){
    yield 'a';
    yield 'b';
}

function * b1(){
    yield 'x';
    a1();
    yield 'y';
}

for(let v of b1()){
    console.log(v);
}

Output results:

x
y

As you can see, it is useless to call A1 in B1.

Modify the above example:

function * a1(){
    yield 'a';
    yield 'b';
}

function * b1(){
    yield 'x';
    yield * a1();
    yield 'y';
}

for(let v of b1()){
    console.log(v);
}

Output results:

x
a
b
y

Synchronous expression of asynchronous operation

The Pause effect of the generator function means that asynchronous operations can be written in the yield statement and executed later when the next method is called. This is actually equivalent to not having to write a callback function, because the subsequent operations of asynchronous operations can be placed under the yield statement, which will be executed when the next method is called anyway. Therefore, an important practical significance of generator function is to handle asynchronous operation and rewrite callback function.

Let’s see how to get an Ajax result through generator.

function * ajaxCall(){
    let result = yield request("http://www.flydean.com");
    let resp = JSON.parse(result);
    console.log(resp.value);
}

function request(url){
    makeAjaxCall(url, function(response){
        it.next(response);
    });
}

var it = ajaxCall();
it.next();

We use a yield to get the result of asynchronous execution. But how do we pass this yield to the result variable? Remember that yield itself has no return value.

We need to call the next method of generator to pass in the result of asynchronous execution. This is what we do in the request method.

Asynchronous application of generator

What is asynchronous application?

The so-called “asynchronism” simply means that a task is not completed continuously. It can be understood that the task is artificially divided into two sections. First, the first section is executed, and then other tasks are executed. When the task is ready, the second section is executed.

For example, there is a task to read a file for processing. The first segment of the task is to send a request to the operating system to read the file. Then, the program performs other tasks, waits until the operating system returns the file, and then performs the second segment of the task (processing the file). This kind of discontinuous execution is called asynchronous.

Accordingly, continuous execution is called synchronization. Because it is a continuous execution, can not insert other tasks, so the operating system read files from the hard disk during this time, the program can only wait.

Before the birth of ES6, there were four methods of asynchronous programming.
Callback function
event listeners
Publish / subscribe
Promise object

Callback function

fs.readFile(fileA, 'utf-8', function(error,data){
    fs.readFile(fileB, 'utf-8', function(error,data){
}
})

If more than two files are read in turn, multiple nesting will occur. Code is not vertical development, but horizontal development, will soon be a mess, unable to manage. Because multiple asynchronous operations form strong coupling, as long as one operation needs to be modified, its upper callback function and lower callback function may need to be modified. This situation is called “callback hell”.

Promise

Promise object is proposed to solve this problem. It is not a new syntax function, but a new writing method, which allows the nesting of callback function to be changed into chain call.

let readFile = require('fs-readfile-promise');
readFile(fileA).then(function(){
    return readFile(fileB);
}).then(function(data){
    console.log(data);
})

Automatic execution of thunk function and asynchronous function

Before talking about the thunk function, we will talk about two ways to call the function, one is to call by value, the other is to call by name.

“Call by value”, that is, before entering the function body, calculate the value of X + 5 (equal to 6), and then pass this value into the function F. C language adopts this strategy.

“Call by name”, that is, the expression x + 5 is directly passed into the function body and evaluated only when it is used.

The compiler’s implementation of “calling by name” usually puts the parameter into a temporary function, and then passes the temporary function into the function body. This temporary function is called the thunk function.

for instance:

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

f(x + 5);

The code above equals:

var thunk = function () {
    return x + 5;
}
function f(thunk){
    return thunk() * 2;
}

In JavaScript language, the function of thunk is not an expression, but a multi parameter function, which is replaced by a single parameter function that only accepts callback function as a parameter.

How to explain?

For example, in nodejs:

fs.readFile(filename,[encoding],[callback(err,data)])

Readfile receives three parameters, among which encoding is optional. Let’s take two parameters for example.

In general, we call this:

fs.readFile(fileA,callback);

Is there any way to rewrite it as a cascading call to a function of a single parameter?

var Thunk = function (fn){
    return function (...args){
        return functon (callback){
            return fn.call(this,...args, callback);
        }
    }
}

var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);

You can see that the above thunk rewrites the function of two parameters into a cascade mode of single parameter function. In other words, thunk is a function that receives a callback and executes the method.

What’s the use of rewriting like this? The thunk function can now be used for automatic process management of the generator function.

Previously, when talking about generator, if there are multiple yield asynchronous methods in generator, we need to pass in the execution results of these asynchronous methods in the next method.

Of course, it’s OK to pass in asynchronous execution results manually. But is there an automatic way?

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

let gen = function * (){
    let r1 = yield readFileThunk('/tmp/file1');
    console.log(r1.toString());

    let r2 = yield readFileThunk('/tmp/file2');
    console.log(r2.toString());
}

let g = gen();

function run(fn){
    let gen = fn();

    function next (err, data){
        let result = gen.next(data);
        if(result.done) return;
        result.value(next);
    }
    next();
}

run(g);

Gen.next returns an object whose value is the thunk function. We pass in next callback to the thunk function again to start the next yield operation.

With this actuator, it is much more convenient to execute the generator function. No matter how many asynchronous operations there are inside, you can directly pass the generator function into the run function. Of course, the premise is that every asynchronous operation must be a thunk function, that is, the thunk function must follow the yield command.

summary

Promise and generator are very important syntax introduced in ES6. The following koa framework is a specific implementation of generator. We will explain the use of KOA in detail in later articles. Please look forward to it.

Author: what about the flydean program

Link to this article:http://www.flydean.com/es6-promise-generator/

Source: flydean’s blog

Welcome to my official account: the most popular interpretation of “those things”, the most profound dry cargo, the most concise tutorial, and many small tricks you don’t know, etc. you’ll find them!