Asynchronous JavaScript programming solutions (Promise + Generaor + Co => Async / Await)

JavaScript and Asynchronous Programming

In order to avoid the complexity of issues such as resource management, javascript is designed as a single-threaded language, even with html5 worker, can not directly access the dom. javascript is designed from the beginning to design a browser GUI programming language, one of the characteristics of GUI programming is to ensure that the UI thread must not be blocked, otherwise poor experience, interface cards and even death. General Android development, there will be an interface to thread a background thread, to ensure the smooth interface. Because javascript is single-threaded, so the use of non-blocking asynchronous programming model, the vast majority of api are asynchronous javascript api. For details on asynchronous programming in JavaScript can refer to my previous article: What is Asynchronous JavaScript programming?

In short, asynchronous programming is too important to the JavaScript language. JavaScript only one thread, if there is no asynchronous programming, simply can not use, not non-stuck. The asynchronous JavaScript programming language to achieve the earliest, that is, the callback function.

Callback

The so-called callback function, the second mandate is to write a separate function which, until re-execute this task, call this function directly. Its English name callback, literal translation is "recall." Read a file for processing, it is written.

fs.readFile('/etc/passwd', function (err, data) {
    if (err) throw err;
    console.log(data);
});

In the above code, the second parameter readFile function is the callback function, which is the task of the second segment. Wait until the operating system returns the / etc / passwd file after the callback function will be performed.

An interesting question is why Node.js convention, the first argument to the callback function must be error object err (if there is no error, this parameter is null)?

The reason is that the implementation is divided into two sections, between these two sections thrown error, the program can not be captured, only as a parameter passed in the second paragraph.

The callback function itself is not the problem, it's a problem in many callbacks nesting. After reading the file is assumed that A, B and then to read the file, and then read the file C, the code is as follows.

fs.readFile(fileA, function (err, data) {
    if (err) throw err;
    console.log(data)
    fs.readFile(fileB, function (err, data) {
        if (err) throw err;
        console.log(data)
        fs.readFile(fileC, function (err, data) () {
            if (err) throw err;
            console.log(data)
        })
    });
});

Not difficult to imagine, if multiple files are read in sequence, there will be multiple nested. Code is not vertical development, but horizontal development, will soon be chaotic, unmanageable. This is called a "callback hell" (callback hell).

So, Promise appeared.

Promise

To clarify that Promisethe concept is not ES6new out, but ES6incorporates a new wording. It is not new syntax features, but a new wording to allow transverse loading callback function, into a longitudinal load. Continuing the above example of the same, using Promisecode into the case:

var readFile = require('fs-readfile-promise');

readFile(fileA)
    .then(function(data){
        console.log(data.toString());    
    })
    .then(function(){
        return readFile(fileB);
    })
    .then(function(data){
        console.log(data.toString());
    })
    .then(function(){
        return readFile(fileC);
    })
    .then(function(data){
        console.log(data.toString());
    })
    .catch(function(err) {
        console.log(err);
    });

NOTE: The above code uses a Nodepackaged Promiseversion of the readFilefunction, its principle is actually returns an Promiseobject that we have simply written a:

var fs = require('fs');

var readFile = function(path) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, (err, data) => {
            if (err) reject(err)
            resolve(data)
        })
    })
}

module.export = readFile

And as such, a common method of operation through a series of Sao eventually become a Promise method of behavior, that is,  Promise of

The above code, I use the  fs-readfile-promise  modules, its role is to return to a version of Promise readFile function. Promise to provide a method to load callback function then, catch method to catch errors thrown during execution.

However, Promisethe wording is just a callback function improved, the use then()after the implementation of two asynchronous tasks to see more clearly, in addition to nothing new. Apart from the advantages, Promisethe biggest problem is the code redundancy, the original task is Promisepackaged, no matter what action, a look are a bunch of then()original semantics become very unclear.

Generator

Coroutine

In the introduction generatorbefore, first tell us about what  coroutines.

The so-called "coroutine", that is, multiple threads cooperate with each other to complete asynchronous task. Coroutine bit like a function, but also a little like a thread. Its operation process is as follows:

  • The first step, coroutine A started.
  • The second step, to perform the coroutine A half into the suspended executive powers transferred to the coroutine B.
  • The third step, (after some time) B return coroutine execution right.
  • The fourth step, coroutine A resumes execution.
function asyncJob() {
    // ... 其他代码
    var f = yield readFile(fileA);
    // ... 其他代码 
}

The above asyncJob()is a coroutine, wherein it is that the secret yieldcommand. It is here that the implementation of the implementation of the right to the other co-routines, in other words, yieldis asynchronous two-stage dividing line. Coroutine encountered yieldcommand to pause until the implementation of the right to return, and then continue to the next execution from where you left off. Its biggest advantage is that the wording of the code much like synchronous operation, if the removal of  yieldthe command, so alike.

Generator concept

GeneratorCoroutine function is achieved in the ES6, the biggest feature is the ability to hand over executive power function (ie suspended). Entire Generatorfunctions of asynchronous tasks is a package, or container that is asynchronous tasks.

function* gen(x) {
    var y = yield x + 2;
    return y;
} 

var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }

The above code, calls Generatorthe function will return a pointer to an internal (i.e. walker) G, which is Generatoranother function different from the ordinary function of place, i.e., it does not return execution result returned is a pointer to the object. Call pointer g of next()method, moves the internal pointer (that is, to perform asynchronous tasks of the first paragraph), points to the first encounter of the yieldstatement.

In other words, next()the role of the method is to be implemented in stages Generatorfunction. Each call next()method returns an object that represents the information (the current stage valueproperties and doneattributes). valueProperty is a yieldvalue statement after expression, represents the value of the current stage; doneattribute is a Boolean value that indicates Generatorwhether the function is finished, that is, whether there is a stage.

Data exchange functions and error handling Generator

GeneratorFunction can suspend and resume execution, which is the fundamental reason that encapsulates asynchronous tasks. In addition, it has two properties, which enables asynchronous programming as a solution: in vivo function of external data exchange and error handling.

next()Method returns a value of valuethe attribute, a Generatordata output function outwardly; next()method can also accept parameters, the Generatorinput data is a function of the body.

function* gen(x) {
    var y = yield x + 2;
    return y;
} 

var g = gen(1);
g.next()      // { value: 3, done: false }
g.next(2)     // { value: 2, done: true }

The above code, the first next()method of the valueattribute, the expression returns x+2the value (3). The second next()method takes two parameters, this parameter can be passed Generatora function, as the upper stage of the asynchronous task returns a result, the body of the function y variables received, so this step valueproperty returns is 2 (the value of variable y).

GeneratorFunction can also be deployed inside the error handling code error functions to capture in vitro thrown.

function* gen(x) {
    try {
        var y = yield x + 2
    } catch(e) {
        console.log(e)
    }
    return y
}

var g = gen(1);
g.next();
g.throw('出错了');

Last line of code, Generatorfunction in vitro, using the pointer to the object throwmethod throws an error, the function in vivo can be try...catch captured code block. This means that the error code and error handling code, to achieve a separation of time and space, which is undoubtedly very important for asynchronous programming.

Usage Generator function

Let's look at how to use the Generator function, perform a real asynchronous tasks.

var fetch = require('node-fetch');

function* gen(){
    var url = 'https://api.github.com/users/github';
    var result = yield fetch(url);
    console.log(result.bio);
}

In the above code, Generator function encapsulates an asynchronous operation, the first read operation of a remote interface, and analysis information from JSON-formatted data. As previously mentioned, this code much like synchronous operation, in addition to adding the yield command.

The method of performing this code as follows.

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

result.value
    .then(function(data){
        return data.json();
    }).then(function(data){
        g.next(data);
    });

The above code, the first function performed Generator obtain visitor object, and then uses the next method (second row), the first phase asynchronous tasks executing. Since  Fetch module returns a Promise object, so the next method invocation then use the next method.

Can be seen that, although the asynchronous operation Generator function represents a very simple, but the process is not easy to manage (i.e., when to perform the first stage, when the second stage is performed). Thus, there is a Co function.

Co Functions

c O library is a well-known programmer TJ Holowaychuk gadgets released in June 2013, to automate Generator function.

For example, there is a Generator function to sequentially read the three files.

var gen = function* (){
    var f1 = yield readFile('/etc/fstab');
    var f2 = yield readFile('/etc/shells');
    var f3 = yield readFile('/etc/services');
    console.log(f1.toString());
    console.log(f2.toString());
    console.log(f3.toString());
}

co library so you do not write the actuator Generator function.

var co = require('co');
co(gen).then(function (){
    console.log('Generator 函数执行完成');
})

In the above code, the function simply pass the co Generator function will be executed automatically. co Promise function returns an object that can be then added with a callback method. In the above code, wait until Generator function execution, one line of output will be prompted.

 The principle of co library

Why co automates Generator function? Previous article said, Generator vessel function is an asynchronous operation. It needs a mechanism to automatically execute when the outcome asynchronous operation, automatically return the executive power. Two ways to do this.

  • Callback. Thunk asynchronous operation will be packaged into a function, return the executive power in the callback function inside.
  • Promise object. The Promise objects packaged into asynchronous operation, then the method returned by the execution right.

co library is actually a combination of two automatic actuator (Thunk function and Promise objects), packaged into a library. The use of co proviso that, yield Generator function command followed, can only be Thunk Promise function or object.

As for what Thunk function, do not start here described, because in the js are packaged into an asynchronous operation speak Promise object, so the next focus function described in co asynchronous operation to be packaged object Promise situation. Of course, if they are interested can refer to: the meaning and function of usage Thunk

with co promise characteristics of the entire package at a Function Generator promise, the use of the next chain Generator, co cycle call method next tasks are different for different sub-package promise. Resolve to perform different depending on the state of the next, thus achieving automatic execution. The basic flow of FIG.

还是沿用上面的例子。首先,把 fs 模块的 readFile 方法 Promise化 一下。

var fs = require('fs');

var readFile = function (fileName){
    return new Promise(function (resolve, reject){
        fs.readFile(fileName, function(error, data){
            if (error) reject(error);
            resolve(data);
        });
    });
};

var gen = function* (){
    var f1 = yield readFile('/etc/fstab');
    var f2 = yield readFile('/etc/shells');
    var f3 = yield readFile('/etc/services');
    console.log(f1.toString());
    console.log(f2.toString());
    console.log(f3.toString());
};

然后,手动执行上面的 Generator 函数。

var g = gen();

g.next().value.then(function(data){
    g.next(data).value.then(function(data){
        g.next(data);
    });
})

手动执行其实就是用 then 方法,层层添加回调函数。理解了这一点,就可以写出一个自动执行器。

function Co(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();
}

Co(gen);

上面代码中,只要 Generator 函数还没执行到最后一步,next 函数就调用自身,以此实现自动执行。其实就是递归啦,原理很简单的。

co 函数库源码

co 就是上面那个自动执行器的扩展,它的源码只有几十行,非常简单。

首先,co 函数接受 Generator 函数作为参数,返回一个 Promise 对象。

function co(gen) {
    var ctx = this;
    return new Promise(function(resolve, reject) {});
}

在返回的 Promise 对象里面,co 先检查参数 gen 是否为 Generator 函数。如果是,就执行该函数,得到一个内部指针对象;如果不是就返回,并将 Promise 对象的状态改为 resolved 。

function co(gen) {
    var ctx = this;
    return new Promise(function(resolve, reject) {
        if (typeof gen === 'function') 
            gen = gen.call(ctx);
        if (!gen || typeof gen.next !== 'function') 
            return resolve(gen);
    });
}

接着,co 将 Generator 函数的内部指针对象的 next 方法,包装成 onFulefilled 函数。这主要是为了能够捕捉抛出的错误。

function co(gen) {
    var ctx = this;
    return new Promise(function(resolve, reject) {
        if (typeof gen === 'function') 
            gen = gen.call(ctx);
        if (!gen || typeof gen.next !== 'function') 
            return resolve(gen);
        onFulfilled();
        function onFulfilled(res) {
            var ret;
            try {
                ret = gen.next(res);
            } catch (e) {
                return reject(e);
            }
            next(ret);
        }    
    });
}

最后,就是关键的 next 函数,它会反复调用自身。

function next(ret) {
    if (ret.done) 
        return resolve(ret.value);
    var value = toPromise.call(ctx, ret.value);
    if (value && isPromise(value)) 
        return value.then(onFulfilled, onRejected);
    return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
})

上面代码中,next 函数的内部代码,一共只有四行命令。

  • 第一行,检查当前是否为 Generator 函数的最后一步,如果是就返回。
  • 第二行,确保每一步的返回值,是 Promise 对象。
  • 第三行,使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数。
  • 第四行,在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。

终极解决Async/Await

异步操作是 JavaScript 编程的麻烦事,麻烦到一直有人提出各种各样的方案,试图解决这个问题。

从最早的回调函数,到 Promise 对象,再到 Generator 函数,每次都有所改进,但又让人觉得不彻底。它们都有额外的复杂性,都需要理解抽象的底层运行机制。

异步I/O不就是读取一个文件吗,干嘛要搞得这么复杂?异步编程的最高境界,就是根本不用关心它是不是异步。

async 函数就是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案。

async 函数是什么

一句话,async 函数就是 Generator 函数的语法糖,同样是执行Generator函数,但是不用显式的调用Co函数了。

看例子:

var fs = require('fs');

var readFile = function (fileName){
    return new Promise(function (resolve, reject){
        fs.readFile(fileName, function(error, data){
            if (error) reject(error);
            resolve(data);
        });
    });
};

var gen = function* (){
    var f1 = yield readFile('/etc/fstab');
    var f2 = yield readFile('/etc/shells');
    var f3 = yield readFile('/etc/services');
    console.log(f1.toString());
    console.log(f2.toString());
    console.log(f3.toString());
};

function Co(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();
}

Co(gen);

写成 async 函数,就是下面这样。

var asyncReadFile = async function (){
    var f1 = await readFile('/etc/fstab');
    var f2 = await readFile('/etc/shells');
    var f3 = await readFile('/etc/services');
    console.log(f1.toString());
    console.log(f2.toString());
    console.log(f3.toString());
};

发现了吧,async函数就是将Generator函数的*替换成了async,将yield替换成await,除此之外,还对 Generator做了以下四点改进:

  • 内置执行器。Generator函数的执行比如靠执行器,所以才有了co模块等异步执行器,而async函数是自带执行器的。也就是说:async函数的执行,与普通函数一模一样,只要一行:
var result = asyncReadFile();
  • 上面的代码调用了asyncReadFile(),就会自动执行,输出最后结果。这完全不像Generator函数,需要调用next()方法,或者使用co模块,才能得到真正执行,从而得到最终结果。
  • 更好的语义。asyncawait比起星号和yield,语义更清楚。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  • 更广的适用性。async函数的await命令后面可以是Promise对象和原始类型的值(数值、字符串和布尔值,而这是等同于同步操作)。
  • 返回值是Promise,这比Generator函数返回的是Iterator对象方便多了。你可以用then()指定下一步操作。

不知道你看到这里,有没有一种...额...算了。反正从Promise => Generaor => Co => Async/Await。所有的代码都是为了异步编程能够优雅的实现,代码量也经历了一个从少到多再到精炼的过程。

async 函数的实现

async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。

async function fn(args){
    // ...
}

// 等同于

function fn(args){ 
    return spawn(function*() {
        // ...
    }); 
}

所有的 async 函数都可以写成上面的第二种形式,其中的 spawn 函数就是自动执行器。下面给出 spawn 函数的实现,基本就是前文自动执行器的翻版。

function spawn(genF) {
    return new Promise(function(resolve, reject) {
        var gen = genF();
        function step(nextF) {
            try {
                var next = nextF();
            } catch(e) {
                return reject(e); 
            }
            if(next.done) {
                return resolve(next.value);
            } 
            Promise.resolve(next.value).then(function(v) {
                step(function() { return gen.next(v); });      
            }, function(e) {
                step(function() { return gen.throw(e); });
            });
        }
        step(function() { 
            return gen.next(undefined); 
        });
    });
}

async 函数的用法

同 Generator 函数一样,async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。下面是一个例子。

async function getStockPriceByName(name) {
    var symbol = await getStockSymbol(name);
    var stockPrice = await getStockPrice(symbol);
    return stockPrice;
}

getStockPriceByName('goog').then(function (result){
    console.log(result);
});

上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象。下面的例子,指定多少毫秒后输出一个值。

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

async function asyncPrint(value, ms) {
    await timeout(ms);
    console.log(value)
}

asyncPrint('hello world', 50);

上面代码指定50毫秒以后,输出"hello world"。

注意

await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。

async function myFunction() {
    try {
        await somethingThatReturnsAPromise();
    } catch (err) {
        console.log(err);
    }
}

// 另一种写法

async function myFunction() {
    await somethingThatReturnsAPromise().catch(function (err){
        console.log(err);
    });
}

await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。

async function dbFuc(db) {
    let docs = [{}, {}, {}];

    // 报错
    docs.forEach(function (doc) {
        await db.post(doc);
    });
}

上面代码会报错,因为 await 用在普通函数之中了。但是,如果将 forEach 方法的参数改成 async 函数,也有问题。

async function dbFuc(db) {
    let docs = [{}, {}, {}];

    // 可能得到错误结果
    docs.forEach(async function (doc) {
        await db.post(doc);
    });
}

上面代码可能不会正常工作,原因是这时三个 db.post 操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用 for 循环。

async function dbFuc(db) {
    let docs = [{}, {}, {}];

    for (let doc of docs) {
        await db.post(doc);
    }
}

如果确实希望多个请求并发执行,可以使用 Promise.all 方法。

async function dbFuc(db) {
    let docs = [{}, {}, {}];
    let promises = docs.map((doc) => db.post(doc));
    let results = await Promise.all(promises);
    console.log(results);
}

// 或者使用下面的写法

async function dbFuc(db) {
    let docs = [{}, {}, {}];
    let promises = docs.map((doc) => db.post(doc));
    let results = [];
    for (let promise of promises) {
        results.push(await promise);
    }
    console.log(results);
}

 

Guess you like

Origin blog.csdn.net/weixin_40851188/article/details/90714369