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')// 获取文件的函数返回一个处理文件异步的promisefunction 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
END
↓↓↓ 三连一下,天天好心情!