Description
JS异步发展史
异步最早的解决方案是回调函数,如事件的回调,setInterval/setTimeout
中的回调。但是回调函数有一个很常见的问题,就是回调地狱的问题(稍后会举例说明);
为了解决回调地狱的问题,社区提出了Promise解决方案,ES6
将其写进了语言标准。Promise
解决了回调地狱的问题,但是Promise
也存在一些问题,如错误不能被try catch,而且使用Promise的链式调用,其实并没有从根本上解决回调地狱的问题,只是换了一种写法。
ES6
中引入Generator
函数,Generator是一种异步编程解决方案,Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权,Generator 函数可以看出是异步任务的容器,需要暂停的地方,都用yield语句注明。但是 Generator 使用起来较为复杂。
ES7
又提出了新的异步解决方案:async/await
,async
是Generator
函数的语法糖,async/await
使得异步代码看起来像同步代码,异步编程发展的目标就是让异步逻辑的代码看起来像同步一样。
Promise常见问题
Q: then
块如何中断?
A: then
块默认会向下顺序执行,return
是不能中断的,可以通过throw
来跳转至catch
实现中断。
Q: Promise
是一种将异步转换为同步的方法吗?
A: 完全不是。Promise
只不过是一种更良好的编程风格。
async / await
async/await 就是 Generator 的语法糖,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成await。
async
async 函数会返回一个 Promise 对象
。如果在函数中 return 一个直接量,async 会把这个直接量通过Promise.resolve()
封装成 Promise 对象。所以在最外层不能用 await 获取其返回值的情况下,我们可以用then() 链来处理这个 Promise 对象。更详细请戳async MDN
Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。
await
async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值。但是 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行
function getSomething() {
return "something";
}
async function testAsync() {
return Promise.resolve("hello async");
}
async function test() {
const v1 = await getSomething();
const v2 = await testAsync();
console.log(v1, v2);
}
test();
如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。
async/await 如何处理异步并发问题的?
Promise 之前
/**
* Promise 之前
*
* 如果使用回调函数实现多个异步并行执行,同一时刻获取最终结果
* 可以借助 发布订阅/观察者模式
*/
let pubsub = {
arry: [],
emit() {
this.arry.forEach(fn => fn());
},
on(fn) {
this.arry.push(fn);
}
}
let data = [];
pubsub.on(() => {
if(data.length === 3) {
console.log(data); //[ '22', 'Yvette', 'engineer' ];数组顺序随机
}
});
fs.readFile('./JS/Async/data/age.txt', 'utf-8', (err, value) => {
data.push(value);
pubsub.emit();
});
fs.readFile('./JS/Async/data/name.txt', 'utf-8', (err, value) => {
data.push(value);
pubsub.emit();
});
fs.readFile('./JS/Async/data/job.txt', 'utf-8', (err, value) => {
data.push(value);
pubsub.emit();
});
使用 Promise
/**
* 将 fs.readFile 包装成promise接口
*/
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if(err) reject(err);
resolve(data);
});
});
}
/**
* 使用 Promise
*
* 通过 Promise.all 可以实现多个异步并行执行,同一时刻获取最终结果的问题
*/
Promise.all([
read('./JS/Async/data/age.txt'),
read('./JS/Async/data/name.txt'),
read('./JS/Async/data/job.txt')
]).then(data => {
console.log(data); //[ '22', 'Yvette', 'engineer' ];数组顺序随机
}).catch(err => console.log(err));
使用 Async/Await
/**
* 使用 Async/Await
*/
async function readAsync() {
let data = await Promise.all([
read('./JS/Async/data/age.txt'),
read('./JS/Async/data/name.txt'),
read('./JS/Async/data/job.txt')
]);
return data;
}
readAsync().then(data => {
console.log(data); //[ '22', 'Yvette', 'engineer' ];数组顺序随机
});
使用 async/await 需要注意什么?
1、await 命令后面的Promise对象,运行结果可能是 rejected,此时等同于 async 函数返回的 Promise 对象被reject。因此需要加上错误处理,可以给每个 await 后的 Promise 增加 catch 方法;也可以将 await 的代码放在try...catch
中。
2、多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
async function f1() {
await Promise.all([
new Promise((resolve) => {
setTimeout(resolve, 600);
}),
new Promise((resolve) => {
setTimeout(resolve, 600);
})
])
}
3、await命令只能用在async函数之中,如果用在普通函数,会报错。
4、async 函数可以保留运行堆栈。
/**
* 函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。
* 等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。
* 如果b()或c()报错,错误堆栈将不包括a()。
*/
function b() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 200)
});
}
function c() {
throw Error(10);
}
const a = () => {
b().then(() => c());
};
a();
/**
* 改成async函数
*/
const a = async () => {
await b();
c();
};
a();