Promise、async 和 await
回调地狱
readFile("example.txt", function(err, contents) { if (err) { throw err; } writeFile("example.txt", function(err) { if (err) { throw err; } console.log("File was written!"); }); });
method1(function(err, result) { if (err) { throw err; } method2(function(err, result) { if (err) { throw err; } method3(function(err, result) { if (err) { throw err; } method4(function(err, result) { if (err) { throw err; } method5(result); }); }); }); });
回调地狱嵌套多个方法调用会创建错综复杂的代码,会难以理解与调试。当想要实现更复杂的功能时,回调函数也会存在问题。
Promise 定义
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise 特点
对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。如果改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
基本用法
Promise 对象是一个构造函数,用来生成 Promise 实例。
const promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } });
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve 函数的作用是,将 Promise 对象的状态从未完成变为成功(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
reject 函数的作用是,将 Promise 对象的状态从未完成变为失败(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数。
promise.then(function(value) { // success}, function(error) { // failure});
then 方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为 resolved 时调用,第二个回调函数是 Promise 对象的状态变为 rejected 时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受 Promise 对象传出的值作为参数。
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve('done'), ms); }); } timeout(100).then((value) => { console.log(value); });
如果调用 resolve 函数和 reject 函数时带有参数,那么它们的参数会被传递给回调函数。 reject 函数的参数通常是 Error 对象的实例,表示抛出的错误;resolve 函数的参数除了正常的值以外,还可能是另一个 Promise 实例。
const p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('fail')), 3000) })const p2 = new Promise(function (resolve, reject) { setTimeout(() => resolve(p1), 1000) }) p2.then(result => console.log(result)).catch(error => console.log(error))// Error: fail
p1 是一个 Promise,3 秒之后变为 rejected。p2 的状态在 1 秒之后改变,resolve 方法返回的是 p1。由于 p2 返回的是另一个 Promise,导致 p2 自己的状态无效了,由 p1 的状态决定 p2 的状态。所以,后面的 then 语句都变成针对后者 p1。又过了 2 秒,p1 变为rejected,导致触发 catch 方法指定的回调函数。
注意,调用 resolve 或 reject 并不会终结 Promise 的参数函数的执行。
new Promise((resolve, reject) => { resolve(1); console.log(2); }).then(r => { console.log(r); });// 2// 1
调用 resolve(1) 以后,后面的 console.log(2) 还是会执行,并且会首先打印出来。这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
一般来说,调用 resolve 或 reject 以后,Promise 的使命就完成了,后继操作应该放到 then 方法里面,而不应该直接写在 resolve 或 reject 的后面。所以,最好在它们前面加上return语句,这样就不会有意外。
new Promise((resolve, reject) => { return resolve(1); // 后面的语句不会执行 console.log(2); })
finally()
finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise .then(result => {···}) .catch(error => {···}) .finally(() => {···});
不管 promise 最后的状态,在执行完 then 或 catch 指定的回调函数以后,都会执行 finally 方法指定的回调函数。
已决的 Promise
const p = Promise.resolve('foo')// 等价于const p = new Promise(resolve => resolve('foo'))const p = Promise.reject('出错了');// 等价于const p = new Promise((resolve, reject) => reject('出错了'))
返回一个状态已经决定的 Promise 实例。
执行器错误
如果在执行器内部抛出了错误,那么 Promise 的拒绝处理函数就会被调用。
let promise = new Promise(function(resolve, reject) { throw new Error("Explosion!"); }); promise.catch(function(error) { console.log(error.message); // "Explosion!"});
串联
let p1 = new Promise(function(resolve, reject) { resolve(42); }); p1.then(function(value) { console.log(value); }).then(function() { console.log("Finished"); });
串联版本的代码中对 p1.then() 的调用返回了第二个 Promise ,又在这之上调用了 then() 。仅当第一个 Promise 已被决议后,第二个 then() 的完成处理函数才会被调用。
响应多个 Promise
Promise.all() 方法
接收单个可迭代对象(如数组)作为参数,并返回一个 Promise 。这个可迭代对象的元素都是 Promise ,只有在它们都完成后,所返回的 Promise 才会被完成。
let p1 = new Promise(function(resolve, reject) { resolve(42); });let p2 = new Promise(function(resolve, reject) { resolve(43); });let p3 = new Promise(function(resolve, reject) { resolve(44); });let p4 = Promise.all([p1, p2, p3]); p4.then(function(value) { console.log(Array.isArray(value)); // true console.log(value[0]); // 42 console.log(value[1]); // 43 console.log(value[2]); // 44});
Promise.race() 方法
接受一个包含需监视的 Promise 的可迭代对象,并返回一个新的 Promise ,但一旦来源 Promise 中有一个被解决,所返回的 Promise 就会立刻被解决。与等待所有 Promise 完成的 Promise.all() 方法不同,在来源 Promise 中任意一个被完成时, Promise.race() 方法所返回的 Promise 就能作出响应。
let p1 = Promise.resolve(42);let p2 = new Promise(function(resolve, reject) { resolve(43); });let p3 = new Promise(function(resolve, reject) { resolve(44); });let p4 = Promise.race([p1, p2, p3]); p4.then(function(value) { console.log(value); // 42});
Promise 缺点
无法取消Promise,一旦新建它就会立即执行,无法中途取消。
如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
当处于pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
async
async 表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
async 函数返回一个 Promise 对象。
async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。
async function f() { return 'hello world'; } f().then(v => console.log(v))// "hello world"
上面代码中,函数 f 内部 return 命令返回的值,会被 then 方法回调函数接收到。
async 函数内部抛出错误,会导致返回的 Promise 对象变为 reject 状态。抛出的错误对象会被 catch 方法回调函数接收到。
async function f() { throw new Error('出错了'); } f().catch( e => console.log(e) )// Error: 出错了
async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
async function asyncTest() { let getUrl = await fetch(); console.log(getUrl); let downloadUrl = await download(); console.log(downloadUrl); return 'complete'; } asyncTest().then(console.log)function fetch() { return new Promise(function(resolve, reject) { setTimeout(function() { resolve('fetchDone'); }, 1000); }); }function download() { return new Promise(function(resolve, reject) { setTimeout(function() { resolve('downloadDone'); }, 2000); }); }
上面代码中,模拟了一个抓取和一个下载,只有这两个操作全部完成,才会执行 then 方法里面的 console.log。
await
正常情况下,await 命令后面是一个 Promise 对象。如果不是,会被转成一个立即 resolve 的 Promise 对象。
async function f() { return await 123; } f().then(v => console.log(v))// 123
await 命令的参数是数值123,它被转成 Promise 对象,并立即 resolve。
await 命令后面的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到。
async function f() { await Promise.reject('出错了'); } f().then(v => console.log(v)).catch(e => console.log(e))// 出错了
注意,上面代码中,await 语句前面没有 return,但是 reject 方法的参数依然传入了 catch 方法的回调函数。这里如果在 await 前面加上 return,效果是一样的。
只要一个 await 语句后面的 Promise 变为 reject,那么整个 async 函数都会中断执行。
async function f() { await Promise.reject('出错了'); await Promise.resolve('hello world'); // 不会执行}
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个 await 放在 try...catch 结构里面,这样不管这个异步操作是否成功,第二个 await 都会执行。
async function f() { try { await Promise.reject('出错了'); } catch(e) { } return await Promise.resolve('hello world'); } f().then(v => console.log(v))// hello world
另一种方法是 await 后面的 Promise 对象再跟一个 catch 方法,处理前面可能出现的错误。
async function f() { await Promise.reject('出错了') .catch(e => console.log(e)); return await Promise.resolve('hello world'); } f().then(v => console.log(v))// 出错了// hello world
注意点
await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。
async function myFunction() { try { await somethingThatReturnsAPromise(); } catch (err) { console.log(err); } }// 另一种写法async function myFunction() { await somethingThatReturnsAPromise() .catch(function (err) { console.log(err); }); }
多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
// 写法一async function test() { let [foo, bar] = await Promise.all([getFoo(), getBar()]); }function getFoo() { return new Promise(function(resolve, reject) { setTimeout(function() { console.log('getFoo'); resolve() }, 2000); }) }function getBar() { return new Promise(function(resolve, reject) { setTimeout(function() { console.log('getBar'); resolve() }, 2000); }) } test()// 写法二async function test() { let fooPromise = getFoo(); let barPromise = getBar(); let foo = await fooPromise; let bar = await barPromise; }
await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。
async function test() { let promises = [function1, function2, function3] promises.forEach(function (fun) { const t = await fun(); console.log(t) }); }function function1() { return new Promise(function(resolve, reject) { setTimeout(function() { resolve('function1') }, 2000) }) }function function2() { return new Promise(function(resolve, reject) { setTimeout(function() { resolve('function2') }, 2000) }) }function function3() { return new Promise(function(resolve, reject) { setTimeout(function() { resolve('function3') }, 2000) }) } test()
如果将 forEach 方法的参数改成 async 函数,也有问题。
promises.forEach(async function (fun) { const t = await fun(); console.log(t) });
上面代码可能不会正常工作,原因是这时三个异步操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用 for 循环。
for (let i of promises) { const t = await i() console.log(t) }
如果确实希望多个请求并发执行,可以使用 Promise.all 方法。当三个请求都 resolved 时,下面两种写法效果相同。
async function dbFuc(db) {async function test() { let promises = [function1(), function2(), function3()] let results = await Promise.all(promises); console.log(results) }
参考链接
作者:Inlight先森
链接:https://www.jianshu.com/p/d95bbcb9a6cf
共同学习,写下你的评论
评论加载中...
作者其他优质文章