JavaScript是单线程语言,但是js中有很多任务耗时比较长,比如ajax请求,如果都按照顺序进行,往往会出现浏览器无响应的情况,所以就需要异步的形式。我下面用Node.js中的读取文件来举例,node和JavaScript原理是差不多的。
先看看下面这个例子:
// a.json
// {
// "next": "b.json",
// "msg": "this is A"
// }
const fs = require('fs')
fs.readFile('a.json', (err, data) => {
if (err) {
console.error(err)
return
}
console.log(data.toString())
})
读取文件内容是个非常简单的异步操作,你得在文件内容读出来之后才能对文件内容进行操作。这是读取一个文件内容的异步过程,只有一个异步函数,回调函数读出的内容也没有拿出来给其他函数使用。
如果是几个异步函数连在一起呢?比如某一个读取文件的操作需要用到上一个读取文件读出来的内容。看下面这个例子:
// a.json
{
"next": "b.json",
"msg": "this is A"
}
// b.json
{
"next": "c.json",
"msg": "this is B"
}
// c.json
{
"next": null,
"msg": "this is C"
}
const fs = require('fs')
// 定义一个读取文件内容的函数
function getFileContent (filename, callback) {
fs.readFile(filename, (err, data) => {
if (err) {
console.error(err)
return
}
// callback是一个函数,参数是文件的内容
callback(JSON.parse(data.toString()))
})
}
getFileContent('a.json', aData => { // aData中拿到a文件的内容,在这个函数里
console.log('a data is', aData) // 才能调用aData.next访问b.json
getFileContent(aData.next, bData => { // 用前面拿到的a文件内容里的b文件名拿到b文件内容,下面继续
console.log('b data is', bData)
getFileContent(bData.next, cData => {
console.log('c data is', cData)
})
})
})
看这里的异步过程产生的回调嵌套已经有好几层了,如果再多几个文件,再多几个异步过程,如山一般的回调就会在代码里形成回调地狱。
这个时候就轮到Promise来了。Promise 是异步编程的一种解决方案,它本身是一个对象,它代表了一个异步操作的最终完成的结果或者失败结果。
promise本身是一个构造函数,下面代码创造了一个Promise
实例:
const promise = new Promise((resolve, reject) => {
// some code
if(/* 异步操作成功 */) {
resolve(value)
} else { /* 异步操作失败(不是抛出错误,是异步条件不满足的情况下reject) */
reject(error)
}
})
promise接受两个参数,resolve, reject,这是两个js自带的函数,不用自己写。resolve的作用是将异步成功的内容传递出去,reject的作用相反,将异步失败时候的内容作为参数传递出去。
promise实例生成后,就可以调用它的then和catch方法,在then中,我们可以拿到resolve和reject中返回的内容,而then又返回一个新的promise实例,这就是它的强大之处,举个例子就好懂了,下面用promise对上面读文件进行重写:
const fs = require('fs')
// 获取文件的函数返回一个处理文件异步的promise
function getFileContent (filename) {
const promise = new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if (err) {
reject(err)
return
}
resolve(JSON.parse(data.toString())) // 将读取出来的文件内容用resolve传递出去
})
})
return promise
}
// 调用这个promise,在then里面拿到了a文件的内容
getFileContent('a.json').then(aData => {
console.log('a Data ', aData)
// 返回一个新的promise,这个promise处理读取b文件的异步
return getFileContent(aData.next)
}).then(bData => { // 返回的是一个promise,所以可以直接链式调用then,这个then会拿到b的内容
console.log('b Data ', bData)
// 返回一个新的promise,这个promise处理读取c文件的异步
return getFileContent(bData.next)
}).then(cData => { // 链式调用
console.log('c Data ', cData)
})
使用promise进行重写后,我们发现,再多的文件只不过是多几个链式调用而已,嵌套控制在一层,解决了回调地狱这个问题。
在上面的链式调用里,后一个then会等待前一个promise的结果产生后调用,如果进行中报错,链式调用就会断掉,这时候就用到promise的另一个方法,catch:
promise
.then(...)
.then(...)
.then(...)
.catch(...) // catch可以处理前面所有then的报错,处理之后可以继续链式调用
.then(...)
一遇到异常抛出,Promise 链就会停下来,直接调用链式中的 catch
处理程序来继续当前执行。这上面的代码看起来或许有点像下面这样:
try {
let result1 = syncDoSomething1();
let result2 = syncDoSomething2(result1);
let result3 = syncDoSomething3(result2);
} catch(error) {
handle(error) // 处理error
}
哦,这样似乎有点同步任务的意思了,正好,在新的规定里可以用新的async/await
语法将这种类似同步编程处理异步逻辑的代码体现出来:
async function foo() {
try {
let result1 = await syncDoSomething1();
let result2 = await syncDoSomething2(result1);
let result3 = await syncDoSomething3(result2);
} catch(error) {
handle(error) // 处理error
}
}
在async定义的函数中,你可以使用await语法来处理promise,await执行到这里之后,会等待promise中的异步处理完成之后直接取出promise的值,然后再继续执行。我们用这种方法再来优化一下我们开头说的读取文件的例子:
async function readFileData() {
const aData = await getFileContent('a.json')
console.log('a Data ', aData)
const bData = await getFileContent(aData.next)
console.log('b Data ', bData)
const cData = await getFileContent(bData.next)
console.log('c Data ', cData)
}
是不是一下子就感觉这个代码非常简单了,就像是在写同步任务一样没有任何回调的顾虑,await会帮我们拿到promise的内容,直接用就可以了。到最后,什么回调地狱完全就没有了。
参考:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises
readFileData 里的 await 调用也可以使用 promise.all 或者 bluebird.props 来管理 promise