介绍
- 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
- 异步任务:不进入主线程,而进入任务队列中的任务,只有任务队列通知主线程,某个异步任务可以执行了,这个任务才会进入主线程执行。
方法:
- 回调 callback
- 承诺 promise
- async/await
1.回调
Node.js 中最早的异步编程概念的直接体现就是回调。它的形式是,为函数A注册一个回调函数B,就把B写在A的最后一个参数来注册。注册后完成任务A后,B就会被调用。例如,我们可以一边读取文件,一边执行其他命令,在文件读取完成后,我们将文件内容作为回调函数的参数返回。这样在执行代码时就没有阻塞或等待文件 I/O 操作。这就大大提高了性能,可以处理大量的并发请求。
缺点在于,这个语法,每个任务只能指定一个回调函数。所以在过去,如果有多重的异步操作的需求,连续几个异步操作之间需要等待上一个操作执行结束再进行下一个操作,会出现如下的代码:(也叫回调地狱)
getData(a => {
getMoreData(a, b => {
getMoreData(b, c => {
getMoreData(c, d => {
getMoreData(d, e => {
console.log(e);
})
})
})
})
})
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
2.Promise / then
为了解决这一问题,ECMAscript 6 提供了 Promise 概念。
Promise 是一个对象,它代表了一个异步操作(代表未来将要发生的事件),以及它的执行状态。我们先来看看 Promise 的构造函数:
new Promise( function(resolve, reject) {...} /* executor */ );
Promise
的构造函数的参数是一个函数,这个函数我们管他叫 executor。executor 带有 resolve
和 reject
两个参数,这两个参数也都分别各是一个 Promise 类函数,各带一个参数 value。原型如下:
Promise.resolve(value)
Promise.reject(value)
先提一下,Promise
有三种状态:初始(pending)``、完成(fulfilled)
、失败(rejected)
。
new 一个 Promise 时,代码是:先做一些异步操作,然后最终会调用resolve
和 reject
两者之一。当异步任务顺利完成且返回,应调用resolve
,而异步任务失败且返回失败原因(通常是一个错误对象)时,会调用reject
函数。
- resolve 方法可以使 Promise 对象的状态改变为成功,在函数内可以传递信息用于后续处理
- reject 方法可以将 Promise 对象的状态改变为失败,在函数内可以传递信息用于后续处理
- 如果异步操作抛出错误,会调用 catch 方法指定的回调函数,处理这个错误,而且then方法指定的回调函数,如果运行中抛出错误,也会被catch捕获
这里的理念是:这让异步方法可以像同步方法那样返回一个东西,但并不是立即返回最终执行结果,而是返回一个代表未来能够出现的结果的 Promise 对象。然后,再根据 Promise 对象的状态,调用不同的回调函数。
下面来说说调用回调函数的方法:Promise.prototype.then()
。作用是为Promise实例添加状态改变时的回调函数,这个方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。then 可以链式调用,原因在于 then 方法本身会返回的也是一个 Promise对象。使用链式调用then,刚刚的回调地狱代码就变成了如下的看起来非常明晰的链式调用的样子:
getData()
.then(a => getMoreData(a))
.then(b => getMoreData(b))
.then(c => getMoreData(c))
.then(d => getMoreData(d))
.then(e => console.log(e));
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
不过大多数情况下,我们不需要自己 new Promise,而是使用已经创建的 Promise 实例对象。
Promise的优缺点如下:有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。缺点是:Promise 构造函数执行时就立即调用 executor 函数,意思是,我们无法取消Promise,一旦新建它就会立即执行,无等待,无法中途取消。且如果不设置回调函数,Promise内部的错误不会反映到外部。
现在我们总结,Promise代表了一个异步操作,保证未来一定会有一个结果,无论结果是好是坏,由状态来区分。相比为某一个函数注册回调函数的好处是,可以将异步对象和回调函数脱离开来,通过then
方法在这个异步操作上绑定回调函数(而不是在函数上注册回调函数),这避免了回调地狱。由于 then 方法返回的是 Promise 对象,所以非常方便我们通过链式调用的方法去解决回调嵌套的问题,而且由于 Promise.all 这样的方法存在,可以让同时执行多个操作变得简单。
3.async/await
async 是 ES7 才有的与异步操作有关的关键字,和 Promise 有很大关联。async 是“异步”的简写,await 可以认为是 async wait 的简写。async 在文档中的完整的名字叫async function
意思是 async 关键字一定是写在函数声明之前的,用于申明一个 function 是异步的。
一句话,async 函数就是 Promise 函数的语法糖。,async 的作用是:使这个函数返回的是一定是个 Promise 对象。
async function testAsync() {
return "hello async";
}
const result = testAsync();
console.log(result); // Promise { 'hello async' }
async 声明的函数,会返回一个 Promise 对象。如果在函数中 return 的不是 Promise 对象而是一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。因为是 Promise 对象,我们自然可以该用原来的方式:then() 链来处理这个 Promise 对象。
testAsync().then(v => {
console.log(v); // 输出 hello async
});
下面来介绍与之对应的 await。await 是一个操作符,语法是这样的:
[return_value] = await expression;
- 其中 expression 可以是一个 Promise 对象或者任何要等待的值。
- (如果等待的不是 Promise 就返回该值本身)。
- 如果是等待 async function/Promise对象,从概念上来讲,就是等待一个异步操作的完成。那么就会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的resolve 函数参数作为 await 表达式的值,然后恢复 async funcion 的执行。(虽然这里出现了阻塞,但实际上代码是异步执行的,async 函数调用/ await 表达式不会造成阻塞,因为它内部所有都被封装在一个 Promise 对象中异步执行。这也是 await 必须用在 async 函数中的原因)
- 若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。
- await 操作符只能在异步函数 async function 中使用,否则是 syntax error。
现在举例,用 setTimeout 模拟耗时的异步操作,先来看看不用 async/await 会怎么写
function takeLongTime() {
return new Promise(resolve => {
setTimeout(() => resolve("long_time_value"), 1000);
});
}
takeLongTime().then(v => {
console.log("got", v);
});
而如果用的是 async/await,则会这样:
function takeLongTime() {
return new Promise(resolve => {
setTimeout(() => resolve("long_time_value"), 1000);
});
} // 这个函数本身return的就是Promise 所以加不加 async 关键字都一样
async function test() {
const v = await takeLongTime();
console.log(v);
}
test();
这两段代码,两种方式对异步调用的处理(实际就是对 Promise 对象的处理)差别并不明显,甚至使用 async/await 还需要多写一些代码,那它的优势到底在哪?答案:async/await 的优势在于处理 then 链。单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。
假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:
/**
* 传入参数 n,表示这个函数执行的时间(毫秒)
* 执行的结果是 n + 200,这个值将用于下一步骤
*/
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
// 现在用 Promise 方式来实现这三个步骤的处理
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
// c:var est>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms
如果用 async/await 来实现呢,会是这样:
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样
总结:async 函数中的 await 表达式,作用就是会使 async 函数暂停执行,等待 Promise 的结果出来,然后恢复async函数的执行并返回解析值。async/await 的目的是简化使用多个 promise 时的同步行为。
Finally
本质上,Promise 是一个被某些函数传出的对象,我们附加回调函数(callback)使用它,不同于“老式”的传入回调,而不是将回调函数传入那些函数内部。通过多次调用 then(),可以添加多个回调函数,它们会按照插入顺序一个接一个独立执行。
Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。
await 一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的可以是任何一个表达式,这个表达式的计算结果是 Promise 对象或者其它值。因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值(Promise里的resolve)——这也可以说是 await 在等 async 函数完成。但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。
如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞(这是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行)后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。
上面已经说明了 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。会使得代码看起来非常清晰得多,几乎跟同步代码一样
reference
- JS 异步(callback→Promise→async/await) - 个人文章 - SegmentFault 思否
- async 函数的含义和用法 - 阮一峰的网络日志
- 理解 JavaScript 的 async/await - 边城客栈 - SegmentFault 思否
- 聊聊同步、异步、阻塞与非阻塞 - 简书
官方文档:
菜鸟教程: