我们将读取文件的readFile方法封装成一个单独的read方法,接受一个path参数

const fs = require("fs")
function read(path)
{
    fs.readFile(path,function(err,data){
        return data;
    })
}
read("a.txt")
read("b.txt")
read("c.txt")

其中x.txt的内容为x
如果我们调用read方法来读取文件 则无法保证输出顺序一定是a,b,c(不考虑readFileSync的情况下)

嵌套回调

fs.readFile(path1 ,function(err,data)){
    console.log(data1);
    fs.readFile(path2 ,function(err,data)){
        console.log(data);
        //更多的回调....
    }
}

将下一个异步操作放到上一个异步操作的回调方法中,这样虽然能保证执行是串行的,但当代码嵌套的层数增加,代码的层次结构就会变得不清晰并且难以维护
回调地狱(callback hell)这个词就被用来描述这种写法

CPS

将回调函数作为参数传递,这种书写方法被称为CPS
下面是一个使用CPS改写的readFile方法:

var callback = function(err,data){
    if(err){
        console.log(err);
        return ;
    }
    console.log(data.toString());
}
fs.readFile("a.txt",callback)

使用CPS来处理多个回调:

const fs = require("fs")
function callback1(err,data){
    console.log(data.toString())
    fs.readFile("b.txt",callback2)
}
function callback2(err,data){
    console.log(data.toString())
    fs.readFile("c.txt",callback3)
}
function callback3(err,data){
    console.log(data.toString())
}
fs.readFile("a.txt",callback1)

其本质上与嵌套回调并无区别 只是看上去更加美观~

使用async模块简化回调

async是一个著名的第三方模块,其初衷是为了解决多个异步调用的嵌套问题

0x01.async.series

const fs = require("fs")
const async = require("async")
function read_a(callback)
{
    fs.readFile("a.txt","utf-8",callback)
}
function read_b(callback)
{
    fs.readFile("b.txt","utf-8",callback)
}
function read_c(callback)
{
    fs.readFile("c.txt","utf-8",callback)
}
async.series([read_a,read_b,read_c],function(err,data){
    console.log(data.toString())
})

series方法接受一个数组和一个回调函数,回调函数的第二个参数是一个数组,包含了全部异步操作的返回结果,结果集中的顺序和series参数数组的顺序是对应的
该方法实际上是嵌套回调的语法糖,所有的异步回调都是顺序执行的,即执行完一个操作再进行下一个操作

0x02.async.parallel

调用方式和参数都与series相同,也会顺序返回所有的调用结果,区别在于所有的方法都是并行执行,执行时间由耗时最长的调用决定
parallel方法在数组中的某个异步调用结束之后没有立刻返回,而是将结果暂存起来,等所有的异步操作完成后,再根据调用顺序将结果组装成顺序的结果集返回

0x03.async.waterfall

同样是顺序执行异步操作,和前两个方法的区别是每一个异步操作都会把结果传递给下一个调用

const fs = require("fs")
const async = require("async")
function read_a(callback)
{
    fs.readFile("a.txt","utf-8",callback)
}
function read_b(value,callback)
{   
    console.log("上一个传入的值:",value)
    fs.readFile("b.txt","utf-8",callback)
}
function read_c(value,callback)
{
    console.log("上一个传入的值:",value)
    fs.readFile("c.txt","utf-8",callback)
}
async.waterfall([read_a,read_b,read_c],function(err,data){
    console.log(data.toString())
})
//输出
// 上一个传入的值: a
// 上一个传入的值: b
// c

0x04.async.map

map和上面的几个方法稍有不同,map接受一个数组作为参数,数组的元素不是方法名而是方法的参数,数组里的值会依次传递给定义的异步函数
map的第二个参数就是异步的方法,无需额外封装

const fs = require("fs")
const async = require("async")
var arr=["a.txt","b.txt","c.txt"]
async.map(arr,fs.readFile,function(err,results){
    console.log(results.toString())
})

然而map方法有一个缺点,就是它只能接受三个参数,分别是一个数组,对应的异步方法和回调函数
以readFile为例,我们会发现没有多余的参数来定义编码格式,这种情况下需要对readFile做一层封装

const fs = require("fs")
const async = require("async")
function myReadFile(path,callback)
{
    fs.readFile(path,"utf-8",callback)
}
var arr=["a.txt","b.txt","c.txt"]
async.map(arr,myReadFile,function(err,results){
    console.log(results.toString())
})

async模块一度是管理异步调用的首选,然而它并不适用所有的场合
有时我们无法在调用前就决定哪些异步方法会被调用。例如使用上一个异步过程的结果来决定下一个调用的异步方法,这时候使用async模块就不是特别方便

Promise

0x01.什么是Promise

可以将Promise理解为一个状态机,它存在下面三种不同的状态,并在某一时刻只能有一种状态

  • Pending : 表示还在执行
  • Fulfilled(或者 resolved) : 执行成功
  • Rejected : 执行失败

一个Promise对应一个操作(通常是异步操作)的封装,异步操作有等待完成,成功,失败三种可能结果,对应了Promise的三种状态
Promise的状态只能由Pending转换为Resolved或者由Pending转换为Rejected,一旦状态转换完成就无法再改变

将异步方法封装成Promise
我们可以用Promise的构造函数来封装一个现有的异步操作

const promise=new Promise(function(resolve , reject))
//...some  code
if(/*异步操作成功*/)
{
    resolve(value);
    }
else{
    reject(error);
}

以读取文件内容的fs.readFile为例,使用Promise封装后的方法如下所示:

const fs = require("fs")
function readFile_Promise(path)
{
    return new Promise(function(resolve,reject){
        fs.readFile(path,"UTF-8",function(err,data){
            if(data)
            {
                resolve(data);
            }else{
                reject(err)
            }
        })
    })
}

简单地来说,一个封装了异步操作的Promise对象实际上并没有做任何事情,它仅仅针对回调函数的不同结果定义了不同的状态
resolve方法和reject方法也没有做多余的事情,仅仅是把异步的结果传递出去
对于异步结果的处理,是交给then方法来完成的
一个then方法通常是如下这种形式:

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

then方法接受两个匿名函数作为参数,它们代表onResolvedonRejected函数
通常来说,如果onRejected的回调方法被调用就表示异步过程中出现错误,这时可以使用catch方法而不是回调函数来处理异常

promise
.then(function(data){
    //success
})
.catch(function(err)){
    //error
};

then方法总是返回一个新的Promise对象,这也就表示对于一个Promise,可以多次调用它的then方法,但由于默认返回的Promise是一个空的对象,除非做一些额外的操作,否则这一操作通常得不到有意义的值

调用两次then方法的结果:

var promise=readFile_Promise("a.txt");
promise.then(function(value){
    console.log(value); // a
}).then(function(value){
    console.log(value); // underfind
}) 

我们可以在readFile的onResolved回调函数中再次调用readFile_promise

var promise=readFile_Promise("a.txt");
promise.then(function(value){
    console.log(value);
    return readFile_Promise("b.txt");
}).then(function(value){
    console.log(value);
})

0x02.Promise的常用API

1.Promise.resolve

Promise提供了resolve方法将一个非Promise对象转换为一个Promise对象

var obj = {
    then:function(){
        console.log("I am a then method");
    }
}
Promise.resolve(obj);  //转换后的Promise方法会自动执行其then方法
// I am a then method

如果转换的对象是一个常量或者不具备状态的语句,转换后的对象自动处于resolve状态,转换的对象作为resolve的结果原封不动地保留

var p = Promise.resolve("Hello World")
p.then(function(result){
    console.log(reuslt);
})
//Hello World

但是resolve不能直接转换异步方法

2.Promise.reject

Promise.reject同样返回一个Promise对象,不同之处在于这个Promise的状态为reject

3.Promise.all

如果有多个Promise需要执行,可以使用promise.all方法统一声明,该方法可以将多个Promise对象包装成一个Promise
该方法接受一个数组作为参数,数据的元素如果不是Promise对象,则会先调用resolve方法转换
只有数组中的Promise的状态全部变成resolved后,all方法生成Promise的状态才会变成resolved,如果中间有一个Promise状态为reject,那么转换后的Promise也会变成reject,并且将错误信息传给catch方法

var promisses = ["a.txt","b.txt","c.txt"]
.map(function(path){
    return readFile_Promise(path);
});
Promise.all(promisses).then(function(results){
    console.log(results);
}).catch(function(err){
    console.log(err);  //results的内容是文本文件内容的顺序排列
})

4.Promise.race

race方法接收一个Promise数组作为参数并返回一个新的Promise,数组中的Promise会同时开始执行,race返回的Promise状态由数组中率先执行完的Promise的状态来决定
一个例子:

function timeout(ms)
{
    return new Promise(function(resolve,reject){
        setTimeout(resolve,ms,'timeout first')
    })
}
let promise = Promise.race([timeout(1),readfile_promise("a.txt")])
promise.then(function(value){
    console.log(value)
})

由于timeout设置为1ms,通常小于读取文件需要的时间,因此调用then方法总是会打印出"timeout first"

Generator

Generator函数和普通函数在外表上最大的区别有两个:

  • 在function关键字和方法名中间有个*
  • 方法体中使用yield关键字
Generator函数若定义了x个yield关键字,那么就有x+1种状态(+1是因为最后的return) 一个例子: ```javascript function* Generator(){ yield "Hello Node"; return "end" } var gen = Generator(); console.log(gen.next()); //{ value: 'Hello Node', done: false } console.log(gen.next()); //{ value: 'end', done: true } console.log(gen.next()); //{ value: undefined, done: true } ``` 当调用Generator函数之后,该函数没有立刻执行,函数的返回结果不是字符串,而是一个对象,可以将该对象理解为一个指针,指向Generator函数当前的状态,直到Generator函数执行完成 当next方法被调用时,Generator函数开始向下执行,遇到yield关键词时,会暂停当前操作,并且对yield后的表达式进行求值,无论yield后面表达式返回的是何种类型的值,yield操作最后返回的都是一个对象,该对象有value和done两个属性 但是yield本身并不产生任何返回值 ```javascript function* foo(x) { var y = yield(x+1); return y; } var gen = foo(5); console.log(gen.next()); //{ value: 6, done: false } console.log(gen.next()); //{ value: undefined, done: true } ```

next方法的返回值是yield关键字后面表达式的值,而yield关键字本身可以视为一个不产生返回值的函数

next方法还可以接受一个数值作为参数,代表上一个yield求值的结果

function* foo(x){
    var y = yield(x+1)
    return y;
}
var gen = foo(5);
console.log(gen.next());   //{ value: 6, done: false }
console.log(gen.next(10)); //{ value: 10, done: true }

上面的代码等价于:

function* foo(x){
    var y = yield(x+1);
    y = 10;
    return y;
}
var gen = foo(5);
console.log(gen.next());
console.log(gen.next());

这个特性使得Generator可以用来组织异步
我们之所以可以使用Generator函数来处理异步任务,原因有二

  • Generator函数可以中断和恢复执行,这个特性由yield关键字来体现
  • Generator函数内外可以交换数据,这个特性由next函数来实现
Generator函数处理异步的核心思想:先将函数暂停在某处,然后拿到异步操作的结果,再把这个结果传到方法体内 yield关键字后面除了通常的函数表达式外,比较常见的是后面跟的一个Promise,由于yield关键字会对其后的表达式求值并返回,那么调用next方法时就会返回一个Promise对象,我们可以调用其then方法,并在回调时使用next方法将结果传回Generator
function* gen()
{
    var result = yield readfile_promise("a.txt");
    console.log(result);
    var result2 = yield readfile_promise("b.txt");
    console.log(result2);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
    g.next(data).value.then(function(data){
        g.next(data);
    })
});

Generator的自动执行

function auto_exec(gen){
    function next(data){
        var result = gen.next(data)
        if(result.done) return result.value;
        result.value.then(function(data){
            next(data)
        })
    }
    next();
}

这个执行器因为调用了then方法,因此只适用于yield后面跟一个Promise的情况
还可以使用co模块自动执行

var co = require("co");
.....
function* gen()
{
    var result = yield readfile_promise("a.txt");
    console.log(result);
    var result2 = yield readfile_promise("b.txt");
    console.log(result2);
}
co(gen);

async/await

async函数可以看作是自带执行器的Generator函数,我们之前有形如下面的Generator方法:

function* gen()
{
    var result = yield readfile_promise("a.txt");
    console.log(result);
    var result2 = yield readfile_promise("b.txt");
    console.log(result2);
}

如果用async函数改写的话,会变成如下形式:

var asyncReadFile = async function(){
    var result1 = await readFile("a.txt");
    var result2 = await readFile("b.txt");
    console.log(result1.toString());
    console.log(result2.toString());
}

形式上看起来没有什么大的变化,yield关键字换成了await 方法名前面的*号变成了async关键字
在使用上的一个区别是await关键字后面往往是一个Promise,如果不是就隐式调用promise.resolve来转换成一个Promise
Await的动作和他的名字含义相同-等待后面的Promise执行完成后再进行下一步操作

0x01.async声明:

async function foo(){}

const foo = async function(){}

const foo = async() =>{}

0x02.async的返回值

async函数总是返回一个Promise对象,如果return关键字后面不是一个Promise,那么默认调用Promise.resolve方法进行转换

async function foo()
{
    return "Hello Node"
}
foo().then(function(data){
    console.log(data)
})

0x03.async函数的执行过程

(1)在async函数开始执行的时候,会自动生成一个Promise对象
(2)当方法体开始执行后,如果遇到return关键字或者throw关键字,执行会立刻退出
(3)执行完毕,返回一个Promise
我们用下面的例子来看看async函数怎么工作

async function asyncFunc()
{
    console.log("begin");
    return "Hello";
}
asyncFunc()
.then(x => console.log(x));
console.log("end");

// 输出
// begin
// end
// Hello

0x04.await关键字

对于async函数来说,await关键词不是必须的。由于async本质上是对Promise的封装,那么可以使用执行Promise的方法来执行一个async方法
而await关键字是对这一情况的语法糖,其可以"自动执行"一个Promise(等待后面的Promise完成后再进行下一步操作)

async function readFile()
{
    var result = await readfile_promise("a.txt");
    console.log(result)
}
readFile()

等价于

readfile_promise("a.txt")
.then(function(data){
    console.log(data);
})

在使用了await关键字之后,无论是代码还是执行,都变得和同步操作没什么两样,这就是await的威力所在

0x05.await与并行

await会等待后面的Promise完成后采取下一步操作,意味着当有多个await操作时,程序会变成完全的串行操作
为了发挥Node的异步优势,当异步操作之间不存在结果依赖关系时,可以使用promise.all来实现并行

async function readFile(){
    const [result1,result2] = await Promise.all([
        readfile_promise("a.txt"),readfile_promise("b.txt")
    ]);
    console.log(result1,result2)
}