从“async”到async——Node异步流程控制总结

Node的异步概念

理解异步非阻塞

提到Node,异步非阻塞会是第一个须要你理解的概念。不少人会把这其实是两个概念的词混为一谈,认为异步就是非阻塞的,而同步就是阻塞的。从实际的效果出发,异步IO和非阻塞IO实际上都能达到咱们对于IO繁重的网络应用并行IO的追求。可是实际上这是两个很不同的概念。html

从操做系统的内核角度出发,I/O调用只有两种方式,阻塞和非阻塞。两者的区别在于,对于使用阻塞IO调用,应用程序须要等待IO的整个过程都所有完成,即完成整个IO目的,此期间CPU进行等待,没法获得充分的利用。而对于使用非阻塞IO调用来讲,应用程序发起IO请求以后不等待数据就当即返回,接下来的CPU时间片可用于其余任务,因为整个IO的过程并无完成,因此还须要使用轮询技术去试探数据是否完整准备好。关于轮询技术细节和发展,此处不过多赘述,很推荐朴灵老师《深刻浅出NodeJs》的第三章。前端

不难理解,从应用程序的角度出发,我无论你操做系统内核是阻塞的IO调用仍是非阻塞的IO调用,只要是我要的数据并无给我,那么这就是同步的,由于我依旧是在等数据。因此对于这种状况下,应用程序的那“一根筋”就能够选择用同步仍是异步的方式去面对该状况。同步即等待操做系统给到数据再进行下面的代码(单线程),异步即发出请求以后也当即返回,用某一种方式注册未完成的任务(回调函数)而后继续往下执行代码。node

理解进程,线程,协程

为了使多个程序可以并发(同一时刻只有一个在运行,时间维度稍微拉长,就会感受起来像多个同时运行)便有了这个在操做系统中可以独立运行并做为资源分配的基本单位git

进程是资源分配的基本单位,进程的调度涉及到的内容比较多(存储空间,CPU,I/O资源等,进程现场保护),调度开销较大,在并发的切换过程效率较低。为了更高效的进行调度,提出了比进程更轻量的独立运行和调度的基本单位线程。最主要的一点同一个进程的多个线程共享进程的资源,这就会暴露出一个多线程编程中须要加入多线程的锁机制来控制资源的互斥性(同时写变量冲突)。线程调度能大幅度减少调度的成本(相对于进程来讲),线程的切换不会引发进程的切换,可是毕竟仍是有成本。es6

面对着线程相关的问题,出现了协程。协程是用户模式下的轻量级线程操做系统内核对协程一无所知,协程的调度彻底有应用程序来控制,操做系统无论这部分的调度。github

协程的特色在因而一个线程执行,所以最大的优点就是协程极高的执行效率。由于子程序切换不是线程切换,而是由程序自身控制,所以,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优点就越明显。第二大优点就是不须要多线程的锁机制,由于只有一个线程,就也不存在同时写变量冲突,在协程中控制共享资源不加锁,只须要判断状态就行了,因此执行效率比多线程高不少。shell

依据上述概念自己咱们可能能够得出一种暂时性的结论:考虑到利用多核CPU,而且充分发挥协程的高效率,又可得到极高的性能,面向开发人员最简单的方法是多进程+协程,既充分利用多核数据库

在Node中利用多核CPU的子进程文档npm

回调函数问题

在Node中每个异步的IO回调函数并非由开发人员所控制主动执行的。编程

那么对于Node的异步IO,在咱们最常使用的异步回调的形式下,咱们发出调用到回调函数执行这中间发生了什么?

整个过程可简单的抽象成四个基本要素:IO线程池观察者请求对象,以及事件循环,盗用《深刻浅出NodeJS》的Windows借用IOCP实现异步回调过程的一张图片:

clipboard.png

其中所要执行的异步回调函数以及相关的全部状态参数会被封装成一个请求对象而后被推入到IO线程池中,当操做系统执行完IO获得结果以后会将数据放入请求对象中,并归还当前线程至线程池,通知IOCP完成了IO过程,而后事件循环IO观察者中获得已经能够执行的请求对象中的回调,灌注IO数据结果开始执行。

Node自己是多线程的,开发人员的JS代码单线程化身为一个老板,实现高效的异步逻辑依靠的是Node机制内部的各个线程池,模拟出了一个异步非阻塞的特色。呈如今开发人员面前的是表现形式为各类各样的callback组成的一个原生编程风格

异步编程与“回调地狱”

const fs = require('fs')

fs.readFile("./test1.txt", "utf-8", function(err,content1){
    if (err) {
        console.log(err)
    } else {
        fs.readFile(content1, "utf-8", function(err,content2){
            if (err) {
                console.log(err);
            } else {
                fs.readFile(content2, "utf-8", function(err,content3){
                    if (err) {
                        console.log(err);
                    } else {
                        console.log(content3)
                    }
                });
            }
        });
    }
});

console.log('主线程')


try {
    console.log(content3)
} catch(e) {
    console.log("尚未获取到content3!");
}

读取的每个 .txt 文件中的内容是要读取的下一个文件的路径地址,最后一个txt文件(test3.txt)中的内容是“callback hell is not finished......”

打印结果:

主线程
尚未获取到content3!
callback hell is not finished......

能够理解为Node代码一根筋的往下想尽快结束所谓的主线程,因此遇到设计异步的就自动忽略并跳过为了往下执行,因此出现了第一句非异步的打印操做,打印“主线程”,再往下执行遇到须要打印 content3 这个变量的时候,主线程就“懵”了,由于命名空间内并无获取到任何 content3 的数据,甚至在主线程命名空间内都没有定义这个变量,若是不用 try-catch 那么应该会报 “content3 is not defined”的错误。

此外,callback hell 尽收眼底,一味地由于依赖而采用嵌套回调函数的方式,哪怕是上述代码那么简单的一个原子性的操做都会被这种“横向发展”的代码和无休止的大括号嵌套让业务逻辑代码丧失掉可维护性和可读性。

为了不这种回调地狱,解决问题的方案和第三方模块就开始层出不穷百花齐放了。

这个async不是ES2017的async

async是一个十分强大,功能十分全面提供异步编程解决法案的一个第三方npm模块。也是我所接触的公司中的项目中大范围使用的。下面是关于这个模块的经常使用函数使用介绍,先感觉一下。

流程控制函数

  • async.parallel(tasks,callback)

    • tasks 能够是一个数组也能够是个对象,他的数组元素值或者对象的属性值就是一个一个异步的方法。

parallel方法用于并行执行多个方法,全部传入的方法都是当即执行,方法之间没有数据传递。传递给最终callback的数组中的数据按照tasks中声明的顺序,而不是执行完成的顺序

//以数组形式传入须要执行的多个方法
async.parallel([
    function(callback){//每一个function均须要传入一个错误优先的callback
        // 异步函数1,好比 fs.readFile(path,callback)
    },
    function(callback){
        // 异步函数2
    }
],
//最终回调 
function(err, results){
    // 当tasks中的任一方法发生错误,即回调形式为callback('错误信息')时,错误将被传递给err参数,未发生错误err参数为空
    if(err){
        console.log(err)
    }else{
        let one = results[0];
        let two = results[1];
        //你的各类操做
    }
    // results中为数组中,两个方法的结果数组:[异步1的结果, 异步2的结果] ,即便第二个方法先执行完成,其结果也是在第一个方法结果以后
});
 
//以object对象形式传入须要执行的多个方法
async.parallel({
    one: function(callback){
        // 异步函数1
    },
    two: function(callback){
        // 异步函数2
    }
},
function(err, results) {
    // 当tasks中的任一方法发生错误,即回调形式为callback('错误信息')时,错误将被传递给err参数,未发生错误err参数为空
    // // results 如今等于: {one: 异步1的结果, two: 异步2的结果}
});
  • 使用时所要注意的事项:

    • 当tasks中的任一方法发生错误时,错误将被传递给最终回调函数的err参数,未发生错误err参数为空。
    • tasks用数组的写法,即便第二个方法先执行完成,其结果也是在第一个方法结果以后,两个方法的结果数组:[异步1的结果, 异步2的结果]

我的感觉:这个方法的大量使用让我以为当一个要展现不少方面的信息的首页时,解耦成了代码可读性的最关键因素,亲身体会的是使用这个方法在企业业务逻辑中理想状况是在 tasks 中注册的并行任务获得的结果最好可以直接使用,而不是在第一个async.parallel的最终回调中依旧须要依赖获得的结果再进行下个系列的异步操做,由于这样致使的结果直接就变成了代码继续向着横向发展,比原生的 callback hell 并无要好到哪里去。篇幅缘由就不展现实际代码了,总之虽然结果流程获得了一个较为明确的控制,可是依旧没有良好的可读性

  • async.series(tasks,callback)

series方法用于依次执行多个方法,一个方法执行完毕后才会进入下一方法,方法之间没有数据传递!!

参数和形式与上面的 async.parallel(tasks,callback)一致

//以数组形式传入须要执行的多个方法
async.series([
    function(callback){
       fs.readFile(path1,callback)
    },
    function(callback){
       fs.readFile(path2,callback)
    }
],
// 可选的最终回调 
function(err, results){
    // 当tasks中的任一方法发生错误,即回调形式为callback('错误信息')时,错误将被传递给err参数,未发生错误err参数为空
    // results中为数组中两个方法的结果数组:['one', 'two'] 
});

这个方法在 tasks 中注册的异步函数之间虽然没有数据传递,可是这个方法控制了这些个异步方法的执行顺序,而且只要一个函数执行失败了接下来的函数就不会再执行了,而且把 err 传递到最终的回调函数中的 err 参数中。正如它的名字 “series”所说,这个方法有点数据库中的事务控制的意思,只不过原生不支持回滚罢了。

  • async.waterfall(tasks,callback)

waterfall方法与series方法相似用于依次执行多个方法,一个方法执行完毕后才会进入下一方法,不一样与series方法的是,waterfall之间有数据传递,前一个函数的输出为后一个函数的输入。waterfall的多个方法只能以数组形式传入,不支持object对象。

async.waterfall([
    function(callback) {
        callback(null, 'one', 'two');
    },
    function(arg1, arg2, callback) {
        // arg1 如今是 'one', arg2 如今是 'two' 
        callback(null, 'three');
    },
    function(arg1, callback) {
        // arg1 如今是 'three' 
        callback(null, 'done');
    }
], function (err, result) {
    //执行的任务中方法回调err参数时,将被传递至本方法的err参数
    // 参数result为最后一个方法的回调结果'done'     
});

由于 tasks 中注册的异步函数数组中前一个函数的输出做为后一个输入,很天然的就能够想到能够经过前一个函数传递“处理成功信号”在第二个函数中进行判断来进行一系列完整的简单相似于事务控制的逻辑操做。

  • async.auto(tasks,callback)

auto方法根据传入的任务类型选择最佳的执行方式。不依赖于其它任务的方法将并发执行,依赖于其它任务的方法将在其执行完成后执行。相似于“依赖注入”概念。

async.auto({
    getData: function(callback){
         //一个取数据的方法
        // 与makeFolder方法并行执行
        callback(null, 'data', 'converted to array');
    },
    makeFolder: function(callback){
        // 一个建立文件夹的方法
        // 与make_folder方法并行执行
        callback(null, 'folder');
    },
    writeFile: ['getData', 'makeFolder', function(callback, results){
        // 此方法在等待getData方法和makeFolder执行完成后执行,而且在results中拿到依赖函数的数据
        callback(null, 'filename');
    }],
    sendEmail: ['writeFile', function(callback, results){
        // 等待writeFile执行完成后执行,results中拿到依赖项的数据
        callback(null, {'file':results.writeFile, 'email':'user@example.com'});
    }]
}, function(err, results) {
    console.log('err = ', err);
    console.log('results = ', results);
});

我的评价:喜欢这种方法,有清晰的可读性,依赖规则以及控制一目了然,很惋惜的是在咱们的代码里面并无使用。缺点是相比较咱们的最终解决方案的优雅,这个仍是会有可能嵌套不少层的大括号的方式有它自己的劣势。

异步集合操做

  • async.each(arr,iterator(item, callback),callback)

对数组arr中的每一项执行iterator操做。iterator方法中会传一个当前执行的项及一个回调方法。each方法中全部对象是并行执行的。对数组中每一项进行 iterator 函数处理,若是有一项出错则最终的回调的 err 就回事该 err。可是,出错并不会影响到其余的数组元素执行。

const async = require('async')
const fs = require('fs')
let arr = ['./Test/file1.txt',"./Test/file2.txt","./Test/file3.txt"]
let iterator = (item,callback)=>{   
        fs.readFile(item,"utf-8",(err,results)=>{
            if(item === "./Test/file2.txt"){
                callback(new Error('wrong'))
            }else{
                console.log(results);
                callback(null,results)
            }          
        })      
}
async.each(arr,iterator,function(err){
    if(err){
        console.log(err)
    }
})

打印结果:

3
Error: wrong
    at fs.readFile (/Users/liulei/Desktop/asyncEach/test.js:10:26)
    at FSReqWrap.readFileAfterClose [as oncomplete] (fs.js:511:3)
1

可见,因为并发的缘由,便是第二项出错,也不会影响其他的元素执行。若是想要让数组中的元素按照顺序执行,而且一旦一个出错,后面的数组元素都将不会执行的状况应该用另外一个函数 async.eachSeeries(arr,iterator(item, callback),callback),用法什么的都同样,这里就不赘述了。

此外,each方法的最终回调函数能够看出来的是,并不会被传入任何结果,因此最终的回调函数就只有一个参数那就是 err,若是想要向最终回调函数中传入某些结果那么还要用到接下来介绍的 asycnc.map()

  • async.map(arr,iterator(item, callback),callback)

map方法使用方式和each彻底同样,与each方法不一样的是,map方法用于操做对象的转换,转换后的新的结果集会被传递至最终回调方法中(不出错的状况下)呈现一个新的数组的形似。

一样的是,map也是并行操做,如需按顺序而且出错就中止则须要使用 async.mapSeries

向Promise的过渡

Promise基础简要介绍

一个简单清晰的例子:

const fs = require('fs')

fs.readFile("./Test/file1.txt", "utf-8", (err, content) => {
    if (err) {
        console.log(err);
    } else {
        console.log(content);
    }
})

let readFile = () => {
    return new Promise((resolve, reject) => {
        fs.readFile("./Test/file2.txt", "utf-8", (err, content) => {
            if (err) {
                reject(err)
            } else {
                resolve(content);
            }
        })
    })
}

readFile()
    .then((res) => {
        console.log(res);
    })
    .catch((err) => {
        console.log(err);
    })

只是比原生的callback形式的异步函数多了一步封装包裹的过程。Promise是一个对象,能够把它看作是一个包含着异步函数可能出现的结果(成功或者失败(err))的“异步状态小球”。获得了这个小球你就能用 then 去弄他,用 catch 去捕获它的失败。简单的归纳,也仅此而已。基于这个小球,咱们就能获得所谓的“现代异步处理方案”了,后话。

前端 Promisify Ajax请求:

let btn = document.getElementById("btn")
let getData = (api) => {
    return new Promise((resolve,reject)=>{
        let req = new XMLHttpRequest();
        req.open("GET",api,true)       
        req.onload = () => {
              if (req.status === 200) {
                resolve(req.responseText)
              } else {
                reject(new Error(req.statusText))
              }
            }
        
        req.onerror = () => {
              reject(new Error(req.statusText))
            }
            req.send()
          })
        }

btn.onclick = function(e) {
    getData('/api')
        .then((res) => {
            let content=JSON.parse(res).msg
            document.getElementById("content").innerText = content
            })
        .catch((err) => {
            console.log(err);
            })
        }

Node提供的原生模块的API基本上都是基于一个 callback 形式的函数,咱们想用 Promise ,难不成甚至原生的这些最原始的函数都要咱们手动去进行 return 一个 Promise 对象的改造?其实不是这样的,Node 风格的 callback 都听从着“错误优先”的回到函数方案,即形如(err,res)=>{},而且回调函数都是最后一个参数,他们的形式都是一致的。因此Node的原生util模块提供了一个方便咱们将函数 Promisfy 的工具——util.promisfy(origin)

let readFileSeccond = util.promisify(fs.readFile)

readFileSeccond("./Test/file3.txt","utf-8")
    .then((res) => {
        console.log(res);
    })
    .catch((err) => {
        console.log(err);
    })

注意,这个原生工具会对原生回调的结果进行封装,若是在最后的回调函数中除了 err 参数以外,还有不止一个结果的状况,那么 util.promisify 会将结果都统一封装进一个对象之中。

用Promise提供方法应对不一样的状况

实际代码逻辑中咱们可能会面对各类异步流程控制的状况,像是以前介绍 async 模块同样,一种很常见的状况就是有不少的异步方法是能够同时并发发起请求的,即互相不依赖对方的结果,async.parallel的效果那样。Promise 除了封装异步以外还未咱们提供了一些原生方法去面对相似这样的状况:

知识准备

  • Promise.resolve(value)

它是下面这段代码的语法糖:

new Promise((resolve)=>{
    resolve(value)
})

注意点,在 then 调用的时候即使一个promise对象是当即进入完成状态的,那Promise的 then 调用也是异步的,这是为了不同步和异步之间状态出现了模糊。因此你能够认为,Promise 只能是异步的,用接下的代码说明:

let promiseA = new Promise((resolve) => {
    console.log("1.构造Promise函数");
    resolve("ray is handsome")
})

promiseA.then((res) => {
    console.log("2.成功态");
    console.log(res);
})

console.log("3.最后书写");

上面的代码,打印的结果以下:

1.构造Promise函数
3.最后书写
2.成功态
ray is handsome

promise 能够链式 then ,每个 then 以后都会产生一个新的 promise 对象,在 then 链中前一个 then 这种能够经过 return的方式想下一个 then 传递值,这个值会自动调用 promise.resolve()转化成一个promise对象,代码说明吧:

const fs = require('fs')
let promise = Promise.resolve(1)
promise
    .then((value) => {
            console.log(value)
            return value+1
    })
    .then((value) => {
            console.log(`first那里传下来的${value}`);
            return value+1
    })
    .then((value) => {
            console.log(`second那里传下来的${value}`);
            console.log(value)
    })
    .catch((err) => {
        console.log(err);
    })

上面的代码答应的结果:

1
first那里传下来的2
second那里传下来的3
3

此外 then 链中应该添加 catch 捕获异常,某一个 then 中出现了错误则执行链会跳事后来的 then 直接进入 catch

获得 async.parallel一样的效果

Promise 提供了一个原生方法 Promise.all(arr),其中arr是一个由 promise 对象组成的一个数组。该方法能够实现让传入该方法的数组中的 promise 同时执行,并在全部的 promise 都有了最终的状态以后,才会调用接下来的 then 方法,而且获得的结果和在数组中注册的结果保持一致。看下面的代码:

const fs = require('fs')
const util = require('util')

let readFile = util.promisify(fs.readFile)

let files = [readFile("../../Test/file1.txt","utf-8"),
            readFile("../../Test/file2.txt","utf-8"),
            readFile("../../Test/file3.txt","utf-8"),]

Promise.all(files)
    .then((res) => {
        console.log(res)
    })
    .catch((err) => {
        console.log(err);
    })

上面的代码最终会打印,便是按顺序的三个txt文件里面的内容组成的数组:

[‘1’,‘2’,‘3’]

对比 async.parallel的用法,发现获得相同的结果。

此外,与 Promise.all方法相对应的还有一个Promise.race,该方法与all用法相同,一样是传入一个由 promise 对象组成的数组,你能够把上面的代码中的 all 直接换成 race 看看是什么效果。没错,对于指导 race 这个英文单词意思的可能已经猜出来了,race 竞争,赛跑,就是只要数组中有一个 promise 到达最终态,该方法的 then 就会执行。因此该代码有可能会出现'1','2','3'中的任何一个字符串。

至此,咱们解决了要改造的代码的第一个问题,那就是多异步的同时执行,那么以前 async 模块介绍的其余的的功能在实际运用中也很常见的几个场景,相似顺序执行异步函数,异步集合操做要怎么使用新的方案模拟出来呢?真正的原生 async要登场了。

所谓的异步流程控制的“终极解决方案”————async

在开始介绍 async 以前,想先聊一种状况。

基于 Promise 的这一套看似可让代码“竖着写”,能够很好的解决“callbackHell”回调地狱的窘境,可是上述全部的例子都是简单场景下。在基于 Promise 的 then 链中咱们不难发现,虽然一层层往下的 then 链能够向下一层传递本层处理好的数据,可是这种链条并不能跨层使用数据,就是说若是第3层的 then 想直接使用第一层的结果必须有一个前提就是第二层不只将本身处理好的数据 return 给第三层,同时还要把第一层传下来的再一次传给第三层使用。否则还有一种方式,那就是咱们从回调地狱陷入另外一种地狱 “Promise地狱”。

借用这篇博客 的一个操做 mongoDB 场景例子说明:

MongoClient.connect(url + db_name).then(db => {
    return db.collection('blogs');
}).then(coll => {
    return coll.find().toArray();
}).then(blogs => {
    console.log(blogs.length);
}).catch(err => {
    console.log(err);
})

若是我想要在最后一个 then 中获得 db 对象用来执行 db.close()关闭数据库操做,我只能选择让每一层都传递这个 db 对象直至我使用操做 then 的尽头,像下面这样:

MongoClient.connect(url + db_name).then(db => {
    return {db:db,coll:db.collection('blogs')};
}).then(result => {
    return {db:result.db,blogs:result.coll.find().toArray()};
}).then(result => {
    return result.blogs.then(blogs => {   //注意这里,result.coll.find().toArray()返回的是一个Promise,所以这里须要再解析一层
        return {db:result.db,blogs:blogs}
    })
}).then(result => {
    console.log(result.blogs.length);
    result.db.close();
}).catch(err => {
    console.log(err);
});

下面陷入 “Promise地狱”:

MongoClient.connect(url + db_name).then(db => {
    let coll = db.collection('blogs');
    coll.find().toArray().then(blogs => {
        console.log(blogs.length);
        db.close();
    }).catch(err => {
        console.log(err);
    });
}).catch(err => {
    console.log(err);
})

看上去不是那么明显,可是已经出现了 then 里面嵌套 then 了,操做一多直接一晚上回到解放前,再一次丧失了让人想看代码的欲望。OK,用传说中的 async 呢

(async function(){
    let db = await MongoClient.connect(url + db_name);
    let coll = db.collection('blogs');
    let blogs = await coll.find().toArray();
    console.log(blogs.length);
    db.close();
})().catch(err => {
    console.log(err);
});

各类异步写的像同步了,async(异步)关键字声明,告诉读代码的这是一个包含了各类异步操做的函数,await(得等它)关键字说明后面的是个异步操做,卡死了等他执行完再往下。这个语义以及视觉确实无法否定这多是“最好的”异步解决方案了吧。

不得不提的 co 模块

众所周知的是 async 函数式 generator 的语法糖,generator 在异步流程控制中的执行依赖于执行器,co 模块就是一个 generator 的执行器,在真正介绍和使用 async 解决法案以前有必要简单了解一下大名鼎鼎的 co 模块。

什么是 generator,详细请参考Ecmascript6 入门

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');
  console.log(f1.toString());
  console.log(f2.toString());
};
// 执行生成器,返回一个生成器内部的指针
var g = gen();
//手动 generator 执行器
g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(data);
  });
})

上述代码采用 generator 的方式在 yeild 关键字后面封装了异步操做并经过 next()去手动执行它。调用 g.next() 是去执行 yield 后面的异步,这个方案就是经典的异步的“协程”(多个线程互相协做,完成异步任务)处理方案。

协程执行步骤:

  1. 协程A开始执行。
  2. 协程A执行到一半,进入暂停,执行权转移到协程B。
  3. (一段时间后)协程B交还执行权。
  4. 协程A恢复执行。

协程遇到 yield 命令就暂停 等到执行权返回,再从暂停的地方继续日后执行。

翻译上述代码:

  • gen()执行后返回一个生成器的内部执行指针,gen 生成器就是一个协程。
  • gen.next()让生成器内部开始执行代码到遇到 yield 执行 yield 后,就暂停该协程,而且交出执行权,此时执行权落到了JS主线程的手里,即开始执行 Promise 的 then 解析。
  • then 的回调里取得了该异步数据结果,调用g.next(data)经过网next()函数传参的形式,将结果返回给生成器的f1变量。
  • 依次回调类推。

说明:

  • g.next()返回一个对象,形如{ value: 一个Promise, done: false }到生成器内部代码执行完毕返回{ value: undefined, done: true }

引出一个问题: 咱们不能每一次用 generator 处理异步都要手写 generator 的 then 回调执行器,该格式相同,每次都是调用.next(),因此能够用递归函数封装成一个函数:

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);

上述执行器的函数编写 co 模块考虑周全的写好了,co模块源码

你只须要:

const co = require('co')
co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res); 
}).catch(onerror);

yield 后面的是并发。

此时咱们来对比 async 写法:)

async function(){
    var res = await [
    Promise.resolve(1),
    Promise.resolve(2)
    ]
    console.log(res);
}().catch(onerror);

async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。而且它不须要额外的执行器,由于它自带 Generator 执行器

本质上其实并无脱离“协程”异步的处理方式

const fs = require('fs')
const util = require('util')


let readFile = util.promisify(fs.readFile);

(async function fn() {
    var a = await readFile('./test1.txt',"utf-8")
    var b = await readFile('./test2.txt',"utf-8")
    console.log(a)
    console.log(b)
})()
.catch((e)=>{
    console.log("出错了")
})



console.log('主线程')

打印结果会先输出“主线程”。

async 解决方案

前文咱们经过 Promise.all()解决了 async.paralle()的功能,如今咱们来看看用 Promise 配合原生 async 来达到“async”模块的其余功能。

  • 实现 async.series 顺序执行异步函数
//源代码
async.series([
        function(callback) {
            if (version.other_parameters != otherParams) { // 更新其余参数
                var newVersion = {
                    id: version.id,
                    other_parameters: otherParams,
                };
                CVersion.update(newVersion, callback);
            } else {
                callback(null, null);
            }
        },
        function(callback) {
            cVersionModel.removeParams(version.id, toBeRemovedParams, callback);
        },
        function(callback) {
            cVersionModel.addParams(version.id, toBeAddedParams, callback);
        },
        function(callback) {
            CVersion.get(version.id, callback);
        },
    ], function(err, results) {
        if (err) {
            logger.error("更新电路图参数失败!");
            logger.error(version);
            logger.error(tagNames);
            logger.error(err);
            callback(err);
        } else {
            callback(null, results[3].parameters);
        }
    });


//新代码

(async function(){
    if (version.other_parameters != otherParams) { // 更新其参数
        var newVersion = {
            id: version.id,
            other_parameters: otherParams,
        };
        await  CVersion.update(newVersion);
    } else {
        return null
    }
    await cVersionModel.removeParams(version.id, toBeRemovedParams)
    await cVersionModel.addParams(version.id, toBeAddedParams)
    let result = await CVersion.get(version.id)
    return result
})()
..catch((err)=>{
    logger.error("更新参数失败!");
    logger.error(version);
    logger.error(tagNames);
    logger.error(err);
})
  • 实现 async.each 的遍历集合每个元素实现异步操做功能:
//源代码
Notification.newNotifications= function(notifications, callback) {
    function iterator(notification, callback) {
        Notification.newNotification(notification, function(err, results) {
            logger.error(err);
            callback(err);
        });
    }

    async.each(notifications, iterator, function(err) {
        callback(err, null);
    });
}

新代码:

//新代码
Notification.newNotifications= function(notifications){
  notifications.forEach(async function(notification){
      try{
           await Notification.newNotification(notification)//异步操做
      } catch (err) {
           logger.error(err);
           return err;
        }    
  });
}

上述代码须要说明的状况是,在forEach 体内的每个元素的 await 都是并发执行的,由于这正好知足了 async.each 的特色,若是你但愿的是数组元素继发执行异步操做,也就是前文所提到的 async.eachSeries 的功能,你须要协程一个 for 循环而不是 forEach 的形式,相似以下代码:

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

  for (let doc of docs) {
    await db.post(doc);//异步数据库操做
  }
}

若是你以为上述并发集合操做使用 forEach 的方式依旧不太直观,也能够改成配合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);
}

上述代码现先对数组元素进行遍历,将传入了数组元素参数的一步操做封装成为一个数组,经过await Promise.all(promises)的形式进行并发操做。Tips: Promise.all 有自动将数组的每一个元素变成Promise对象的能力。

相关文章
相关标签/搜索