众所周知,在ES6推出之前,要想执行一串有前后依赖关系的异步操作需要通过纯回调函数的方式,即把外层的函数所得到的结果作为参数传入到下一层函数, 试着看这样一个例子:
根目录下有一个index.js脚本(也就是我们要编写读取文件的脚本),要读取 /files 文件夹里的三个json文件,分别为:
a.json
{ "next" : "b.json", "msg": "this is a" }b.json
{ "next" : "b.json", "msg": "this is a" }c.json
{ "next" : "null", "msg": "this is a" }可以看出这三个文件之间有着链式的关系,a.json的next属性指向b.json,同样b.json的next属性指向c.json,要异步读取这三个文件,我们可以采用下面这种方式:
这种老千层饼式的调用大大降低了代码的可读性!!!而且在异常处理上,每一层都有自己的错误回调函数,不方便管理。
同时注意,下面这种避免回调地狱的方式是错误的,是对JavaScript 单线程异步机制的误解。下面这段代码,妄图在读取完aData之后将aData.next作为参数传递给下一个getFileContent函数,继续读取bData。但是,由于读取aData的fs.readFile是异步操作,需要等待js的主线程的callstack执行完才会把结果传回来,这个过程中就包括了执行读取a.next的getFileContent。当主线程的callstackpop到getFileContent(global.a.next,bData=>...}时,傻眼了,因为此时读取aData的结果还在callback stack里候着呢,所以a是undefined,哪里有next属性…于是出发了TypeError
getFileContent('a.json', aData => { console.log(aData) global.a = aData }, err => { console.log(err) }) getFileContent(global.a.next,bData=>{ //TypeError,因为 console.log(bData) global.b = bData },err =>{ console.log(err) }) getFileContent(global.b.next,cData=>{ console.log(cData) global.c = cData },err =>{ console.log(err) })Promise 是异步编程的新解决方案(旧的就是刚才说的纯回调机制)。Promise对象有三个状态:pending(正在执行),resolved(执行成功),rejected(执行失败)。这三个状态之间只能有两个转移方式:
pending 变为 resolvedpending 变为 rejected且一个promise对象只能改变一次。
无论成功和失败,promise都会有都会有一个结果数据,成功的数据一般为value;失败的结果一般为reason。
Promise的运行流程可以参考下图: 首先,Promise()函数本身的回调函数(resolve,reject)=>void是一个同步回调,也就是说,promise对象一产生就会执行它绑定的回调函数。而resolve和reject则所绑定的回调函数(通过.then()的方式绑定)因为异步的原因,在同步任务执行完成才会执行。具体可以参考这个例子: let promise = new Promise(function(resolve, reject) { console.log('Promise'); resolve(); }); promise.then(function() { console.log('resolved.'); }); console.log('Hi!'); // Promise // Hi! // resolved具体到读取文件的任务,我们可以用ES6中的新构造函数:Promise 来创建一个promise对象,并用下面这个模板来解决上述的读取文件的问题:
//方案二 promise异步读取文件 //最外层是封装好的执行任务的函数getFileContent function getFileContent(filename) { //第一件事就是新建一个Promise对象,并绑定回调函数 const promise = new Promise((resolve, reject) => { const fullName = path.resolve(__dirname, 'files', filename) fs.readFile(fullName, (err, data) => { if (err) { reject(err) return } resolve(JSON.parse(data.toString())) }) }) return promise } getFileContent('a.json').then((aData) => { console.log(aData) return getFileContent(aData.next) }).then((bData)=>{ console.log(bData) return getFileContent(bData.next) }).then((cData)=>{ console.log(cData) }).catch((err)=>{ console.log(err) }) 可以看到,由于每次执行getFileContent()都会返回一个promise对象,所以可以直接在getFileContent('a.json')的后面用.then()来绑定回调函数。为了链式调用,每次执行.then()回调函数都会调用下一个getFileContent()并把返回的promise对象暴露给下一个.then()最后,用一个.catch()来统一管理链式调用中所产生的错误,无论是哪一个promise出错了,那么这个调用环就断了(因为这里每一环.then()都只写了第一个参数,即onresolved的回调,并没有指定第二个参数,即onrejected函数),所以异常传透给最后的.catch统一处理。