ES6 系列之我们来聊聊 Async
mqyqingfeng opened this issue · comments
async
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
在异步处理上,async 函数就是 Generator 函数的语法糖。
举个例子:
// 使用 generator
var fetch = require('node-fetch');
var co = require('co');
function* gen() {
var r1 = yield fetch('https://api.github.com/users/github');
var json1 = yield r1.json();
console.log(json1.bio);
}
co(gen);
当你使用 async 时:
// 使用 async
var fetch = require('node-fetch');
var fetchData = async function () {
var r1 = await fetch('https://api.github.com/users/github');
var json1 = await r1.json();
console.log(json1.bio);
};
fetchData();
其实 async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
spawn 函数指的是自动执行器,就比如说 co。
再加上 async 函数返回一个 Promise 对象,你也可以理解为 async 函数是基于 Promise 和 Generator 的一层封装。
async 与 Promise
严谨的说,async 是一种语法,Promise 是一个内置对象,两者并不具备可比性,更何况 async 函数也返回一个 Promise 对象……
这里主要是展示一些场景,使用 async 会比使用 Promise 更优雅的处理异步流程。
1. 代码更加简洁
/**
* 示例一
*/
function fetch() {
return (
fetchData()
.then(() => {
return "done"
});
)
}
async function fetch() {
await fetchData()
return "done"
};
/**
* 示例二
*/
function fetch() {
return fetchData()
.then(data => {
if (data.moreData) {
return fetchAnotherData(data)
.then(moreData => {
return moreData
})
} else {
return data
}
});
}
async function fetch() {
const data = await fetchData()
if (data.moreData) {
const moreData = await fetchAnotherData(data);
return moreData
} else {
return data
}
};
/**
* 示例三
*/
function fetch() {
return (
fetchData()
.then(value1 => {
return fetchMoreData(value1)
})
.then(value2 => {
return fetchMoreData2(value2)
})
)
}
async function fetch() {
const value1 = await fetchData()
const value2 = await fetchMoreData(value1)
return fetchMoreData2(value2)
};
2. 错误处理
function fetch() {
try {
fetchData()
.then(result => {
const data = JSON.parse(result)
})
.catch((err) => {
console.log(err)
})
} catch (err) {
console.log(err)
}
}
在这段代码中,try/catch 能捕获 fetchData() 中的一些 Promise 构造错误,但是不能捕获 JSON.parse 抛出的异常,如果要处理 JSON.parse 抛出的异常,需要添加 catch 函数重复一遍异常处理的逻辑。
在实际项目中,错误处理逻辑可能会很复杂,这会导致冗余的代码。
async function fetch() {
try {
const data = JSON.parse(await fetchData())
} catch (err) {
console.log(err)
}
};
async/await 的出现使得 try/catch 就可以捕获同步和异步的错误。
3. 调试
const fetchData = () => new Promise((resolve) => setTimeout(resolve, 1000, 1))
const fetchMoreData = (value) => new Promise((resolve) => setTimeout(resolve, 1000, value + 1))
const fetchMoreData2 = (value) => new Promise((resolve) => setTimeout(resolve, 1000, value + 2))
function fetch() {
return (
fetchData()
.then((value1) => {
console.log(value1)
return fetchMoreData(value1)
})
.then(value2 => {
return fetchMoreData2(value2)
})
)
}
const res = fetch();
console.log(res);
因为 then 中的代码是异步执行,所以当你打断点的时候,代码不会顺序执行,尤其当你使用 step over 的时候,then 函数会直接进入下一个 then 函数。
const fetchData = () => new Promise((resolve) => setTimeout(resolve, 1000, 1))
const fetchMoreData = () => new Promise((resolve) => setTimeout(resolve, 1000, 2))
const fetchMoreData2 = () => new Promise((resolve) => setTimeout(resolve, 1000, 3))
async function fetch() {
const value1 = await fetchData()
const value2 = await fetchMoreData(value1)
return fetchMoreData2(value2)
};
const res = fetch();
console.log(res);
而使用 async 的时候,则可以像调试同步代码一样调试。
async 地狱
async 地狱主要是指开发者贪图语法上的简洁而让原本可以并行执行的内容变成了顺序执行,从而影响了性能,但用地狱形容有点夸张了点……
例子一
举个例子:
(async () => {
const getList = await getList();
const getAnotherList = await getAnotherList();
})();
getList() 和 getAnotherList() 其实并没有依赖关系,但是现在的这种写法,虽然简洁,却导致了 getAnotherList() 只能在 getList() 返回后才会执行,从而导致了多一倍的请求时间。
为了解决这个问题,我们可以改成这样:
(async () => {
const listPromise = getList();
const anotherListPromise = getAnotherList();
await listPromise;
await anotherListPromise;
})();
也可以使用 Promise.all():
(async () => {
Promise.all([getList(), getAnotherList()]).then(...);
})();
例子二
当然上面这个例子比较简单,我们再来扩充一下:
(async () => {
const listPromise = await getList();
const anotherListPromise = await getAnotherList();
// do something
await submit(listData);
await submit(anotherListData);
})();
因为 await 的特性,整个例子有明显的先后顺序,然而 getList() 和 getAnotherList() 其实并无依赖,submit(listData) 和 submit(anotherListData) 也没有依赖关系,那么对于这种例子,我们该怎么改写呢?
基本分为三个步骤:
1. 找出依赖关系
在这里,submit(listData) 需要在 getList() 之后,submit(anotherListData) 需要在 anotherListPromise() 之后。
2. 将互相依赖的语句包裹在 async 函数中
async function handleList() {
const listPromise = await getList();
// ...
await submit(listData);
}
async function handleAnotherList() {
const anotherListPromise = await getAnotherList()
// ...
await submit(anotherListData)
}
3.并发执行 async 函数
async function handleList() {
const listPromise = await getList();
// ...
await submit(listData);
}
async function handleAnotherList() {
const anotherListPromise = await getAnotherList()
// ...
await submit(anotherListData)
}
// 方法一
(async () => {
const handleListPromise = handleList()
const handleAnotherListPromise = handleAnotherList()
await handleListPromise
await handleAnotherListPromise
})()
// 方法二
(async () => {
Promise.all([handleList(), handleAnotherList()]).then()
})()
继发与并发
问题:给定一个 URL 数组,如何实现接口的继发和并发?
async 继发实现:
// 继发一
async function loadData() {
var res1 = await fetch(url1);
var res2 = await fetch(url2);
var res3 = await fetch(url3);
return "whew all done";
}
// 继发二
async function loadData(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
async 并发实现:
// 并发一
async function loadData() {
var res = await Promise.all([fetch(url1), fetch(url2), fetch(url3)]);
return "whew all done";
}
// 并发二
async function loadData(urls) {
// 并发读取 url
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
async 错误捕获
尽管我们可以使用 try catch 捕获错误,但是当我们需要捕获多个错误并做不同的处理时,很快 try catch 就会导致代码杂乱,就比如:
async function asyncTask(cb) {
try {
const user = await UserModel.findById(1);
if(!user) return cb('No user found');
} catch(e) {
return cb('Unexpected error occurred');
}
try {
const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
} catch(e) {
return cb('Error occurred while saving task');
}
if(user.notificationsEnabled) {
try {
await NotificationService.sendNotification(user.id, 'Task Created');
} catch(e) {
return cb('Error while sending notification');
}
}
if(savedTask.assignedUser.id !== user.id) {
try {
await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
} catch(e) {
return cb('Error while sending notification');
}
}
cb(null, savedTask);
}
为了简化这种错误的捕获,我们可以给 await 后的 promise 对象添加 catch 函数,为此我们需要写一个 helper:
// to.js
export default function to(promise) {
return promise.then(data => {
return [null, data];
})
.catch(err => [err]);
}
整个错误捕获的代码可以简化为:
import to from './to.js';
async function asyncTask() {
let err, user, savedTask;
[err, user] = await to(UserModel.findById(1));
if(!user) throw new CustomerError('No user found');
[err, savedTask] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
if(err) throw new CustomError('Error occurred while saving task');
if(user.notificationsEnabled) {
const [err] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
if (err) console.error('Just log the error and continue flow');
}
}
async 的一些讨论
async 会取代 Generator 吗?
Generator 本来是用作生成器,使用 Generator 处理异步请求只是一个比较 hack 的用法,在异步方面,async 可以取代 Generator,但是 async 和 Generator 两个语法本身是用来解决不同的问题的。
async 会取代 Promise 吗?
-
async 函数返回一个 Promise 对象
-
面对复杂的异步流程,Promise 提供的 all 和 race 会更加好用
-
Promise 本身是一个对象,所以可以在代码中任意传递
-
async 的支持率还很低,即使有 Babel,编译后也要增加 1000 行左右。
参考
- [译] 6 个 Async/Await 优于 Promise 的方面
- [译] 如何逃离 async/await 地狱
- 精读《async/await 是把双刃剑》
- ECMAScript 6 入门
- How to write async await without try-catch blocks in Javascript
ES6 系列
ES6 系列目录地址:https://github.com/mqyqingfeng/Blog
ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
消灭零评论 ^_^
如果 await 后面跟的不是 promise ,那么是不是,就不是所谓的顺序执行了!
最后2个"async 会取代 Promise 吗?" 看着怪怪的
@mqyqingfeng 想问一下题主,async/await
到底应该算是 ES7(2016) 还是 ES8 (2017) 呢?
网上有说是 ES7 的,也有说是 ES8 的 :-)
如果 await 后面跟的不是 promise ,那么是不是,就不是所谓的顺序执行了!
如果 await 后面的 x 不是 promise 会转换为 promise.resolve(x),也就是还是返回的 promise
@mqyqingfeng 想问一下题主,
async/await
到底应该算是 ES7(2016) 还是 ES8 (2017) 呢?网上有说是 ES7 的,也有说是 ES8 的 :-)
ES8 (2017) 在babel官网有
// 并发二
async function loadData(urls) {
// 并发读取 url
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
map 循环里是不是不该加 await ,才能实现并发读取
@heyunjiang 都是对的,都可以实现。
async看了有三遍了,一直忘,总是感觉很奇怪下面这段代码
async function setTime(ms) {
await setTimeout(() => {console.log('aaaaaaaaa')}, ms)
}
async function print(val, ms) {
await setTime(ms)
console.log(val)
}
print('vvvvvvvv', 3000);
// vvvvvvvv
// Promise{<resolved>: undefined}
// aaaaaaaaa
像上面的代码,为何直接await setTimeout,print函数没有等setTimeout执行完毕后再打印?
请各位大佬解解惑哈~
好吧,还是我自己不够理解啊。。。
await后面如果跟一个promise对象,那么会等该promise resolve后才会继续下面的执行,但是,我这里的代码,直接await setTimeout,不是一个promise,也就不会等它执行完才会执行下面的代码了。
async function test() {
const tt = await setTimeout(() => {console.log(333)}, 3000)
console.log(444)
console.log(tt)
}
test()
// 444
// 562
// Promise{<resolved>: undefined}
// 333
@lizhongzhen11 这个地方你可以想象下,await 后面直接接个异步的函数,比如你例子当中的 setTimeout 方法,那后面的程序什么时候知道你这个异步的方法被执行完了呢?promise 就不一样了,虽然也是异步执行,但是这个 promise 被 resolve 后就表明这个 promise (异步任务)执行结束了。那么就可以开始执行下面的方法了。
async function test() { const tt = await setTimeout(() => {console.log(333)}, 3000) console.log(444) console.log(tt) } test() // 444 // 562 // Promise{<resolved>: undefined} // 333
不是promise会转为promise,resolved后就继续往下执行了
async function test() {
const tt = await Promise.resolve(setTimeout(() => {console.log(333)}, 3000))
console.log(444)
console.log(tt)
}
test()
看完后感觉理解的又深了不少~很赞
== 0 ==
async function test0() {
await setTimeout(() =>console.log(111), 3000)
console.log(222)
}// 222 111
// 问题提出:为啥await没有起作用?await不是返回一个promise,当promise决议后才下一步吗?
== 1 ==
async function test1() {
await new Promise(resolve => setTimeout(()=>console.log(111), 1000))
// 如果没有resolve 就不会往下执行
console.log(222)// 未执行
}// 111
== 2 ==
async function test2() {
await new Promise(resolve => setTimeout(()=>resolve(console.log(111)), 1000))
console.log(222)
}// 111 222 正常版本
== 3 ==
async function test3() {
await Promise.resolve(setTimeout(()=>console.log(111),500));
console.log(222)
}// 222 111 因为 await后面是一个已经resolve的promise 所以直接下一步
== 4 ==
async function test4() {
await Promise.all([setTimeout(()=>console.log(111),500)]);
console.log(222)
}// 222 111 当all的数组元素不是promise的时候 会调用Promise.resolve方法包装 产生与3一样结果
== 5 ==
async function test5(){
await new _Promise(resolve=>setTimeout(()=>resolve(console.log(111)),1000));
console.log(222)
}// 111 222 _Promise是自己模拟的promise
由上,
1、await后跟的函数调用立即被调用(虽然听起来很像废话..
async function test6() {
await setTimeout(()=>console.log(1),1000);
await setTimeout(()=>console.log(2),500);
await console.log(3)
}// 3 2 1
当 await后面跟的不是promise是普通函数的话,后面的普通函数实际上看成由Promise.resolve包裹后执行
再看下面这个例子更清晰:
test7();
async function test7() {
console.log(1)
await console.log(2)
console.log(3)
}
console.log(4)
// 1 2 4 3
test7 等价于 test7_1
test7_1()
async function test7_1() {
console.log(1)
await Promise.resolve(console.log(2));
console.log(3)
}
console.log(4)
// 1 2 4 3
进而等价于 test7_2
function test7_2() {
console.log(1)
Promise.resolve(console.log(2)).then(()=>{console.log(3)})
}
test7_2();
而产生阻塞的情况是:await后面是未被决议的promise对象才会产生阻塞
(如果是自己写的promise也会产生和Promise一样的效果)
代码运行环境是:node v15.9.0;
感谢评论区的 @jesse-li 指出问题,之前的代码解释有点问题已修正
补充阻塞情况的代码
test8();
async function test8() {
await new Promise(resolve => {
console.log(1);
setTimeout(() => {
// resolve();
}, 100);
});
// 由于上一个promise未决议,下面的代码不会执行
await new Promise(resolve => {
console.log(2);
setTimeout(() => {
resolve();
}, 100);
});
await console.log(3);
console.log(5); // 5 也不会执行哦
}
// 但并不影响函数外的执行
console.log(6);
输出:1、6
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里
既然自执行了,await为何会阻塞后面代码执行,async+await ,还有Generator属于微任务或宏任务吗?
async看了有三遍了,一直忘,总是感觉很奇怪下面这段代码
async function setTime(ms) { await setTimeout(() => {console.log('aaaaaaaaa')}, ms) } async function print(val, ms) { await setTime(ms) console.log(val) } print('vvvvvvvv', 3000); // vvvvvvvv // Promise{<resolved>: undefined} // aaaaaaaaa像上面的代码,为何直接await setTimeout,print函数没有等setTimeout执行完毕后再打印?
请各位大佬解解惑哈~好吧,还是我自己不够理解啊。。。
await后面如果跟一个promise对象,那么会等该promise resolve后才会继续下面的执行,但是,我这里的代码,直接await setTimeout,不是一个promise,也就不会等它执行完才会执行下面的代码了。
我理解的:如果不是promise,会自动转成立即resolved的promise.因为上面立即resolved了,所以直接执行下面代码了。
真的干货满满
(async () => {
const listPromise = getList();
const anotherListPromise = getAnotherList();
await listPromise;
await anotherListPromise;
})();
这个写法有点不理解,就是 getList 和 getAnotherList。返回的是promise。如果后面有代码要执行,就要等这两个请求回来了再执行?
原来to.js就封装了一个简单的promise函数,我们项目里面居然用的npm中的库,学习了。
(async () => { const listPromise = getList(); const anotherListPromise = getAnotherList(); await listPromise; await anotherListPromise; })();这个写法有点不理解,就是 getList 和 getAnotherList。返回的是promise。如果后面有代码要执行,就要等这两个请求回来了再执行?
@chenhuiYj 我觉得是的,这样的写法只是为了让两个异步函数并行执行,而不是一先一后执行,下面的await是用来获取上面异步函数的返回数据。但是程序还是要等到上面两个函数都执行完毕,才会执行await下面的其他代码的。
== 0 == async function test() { await setTimeout(() =>console.log(111), 3000) console.log(222) }// 222 111 // 问题提出:为啥await没有起作用?await不是返回一个promise,当promise决议后才下一步吗? == 1 == async function test() { await new Promise(resolve => setTimeout(()=>console.log(111), 1000)) // 如果没有resolve 就不会往下执行 console.log(222)// 未执行 }// 111 == 2 == async function test() { await new Promise(resolve => setTimeout(()=>resolve(console.log(111)), 1000)) console.log(222) }// 111 222 正常版本 == 3 == async function test() { await Promise.resolve(setTimeout(()=>console.log(111),500)); console.log(222) }// 222 111 因为 await后面是一个已经resolve的promise 所以直接下一步 == 4 == async function test() { await Promise.all([setTimeout(()=>console.log(111),500)]); console.log(222) }// 222 111 当all的数组元素不是promise的时候 会调用Promise.resolve方法包装 产生与3一样结果 == 5 == async function test(){ await new _Promise(resolve=>setTimeout(()=>resolve(console.log(111)),1000)); console.log(222) }// 111 222 _Promise是自己模拟的promise由上,
1、await后跟的函数调用立即被调用(虽然听起来很像废话..async function test() { await setTimeout(()=>console.log(1),1000); await setTimeout(()=>console.log(2),500); await console.log(3) }// 3 2 1但并不代表执行全是同步的,请看下列代码的输出:
async function test() { console.log(1) await console.log(2) console.log(3) } console.log(4) // 1 3 4 2因为归根结底 await后面的普通函数实际上看成由Promise.resolve包裹后执行的
async function test() { console.log(1) await Promise.resolve(console.log(2)); console.log(3) } console.log(4) // 1 3 4 22、await后面是promise对象且promise未被决议才会产生阻塞
(如果是自己写的promise也会产生和Promise一样的效果)
输出有问题吧,如果await后的promise已经决议,就应该是同步了啊。我开始一直没理解为啥决议了还是异步。
async function test() { console.log(1) await Promise.resolve(console.log(2)); console.log(3) } test(); console.log(4)
相当于
function test() { console.log(1) Promise.resolve(console.log(2)).then(()=>{console.log(3)}) } test(); console.log(4)
应该输出 1 2 4 3
== 0 == async function test() { await setTimeout(() =>console.log(111), 3000) console.log(222) }// 222 111 // 问题提出:为啥await没有起作用?await不是返回一个promise,当promise决议后才下一步吗? == 1 == async function test() { await new Promise(resolve => setTimeout(()=>console.log(111), 1000)) // 如果没有resolve 就不会往下执行 console.log(222)// 未执行 }// 111 == 2 == async function test() { await new Promise(resolve => setTimeout(()=>resolve(console.log(111)), 1000)) console.log(222) }// 111 222 正常版本 == 3 == async function test() { await Promise.resolve(setTimeout(()=>console.log(111),500)); console.log(222) }// 222 111 因为 await后面是一个已经resolve的promise 所以直接下一步 == 4 == async function test() { await Promise.all([setTimeout(()=>console.log(111),500)]); console.log(222) }// 222 111 当all的数组元素不是promise的时候 会调用Promise.resolve方法包装 产生与3一样结果 == 5 == async function test(){ await new _Promise(resolve=>setTimeout(()=>resolve(console.log(111)),1000)); console.log(222) }// 111 222 _Promise是自己模拟的promise由上,
1、await后跟的函数调用立即被调用(虽然听起来很像废话..async function test() { await setTimeout(()=>console.log(1),1000); await setTimeout(()=>console.log(2),500); await console.log(3) }// 3 2 1但并不代表执行全是同步的,请看下列代码的输出:
async function test() { console.log(1) await console.log(2) console.log(3) } console.log(4) // 1 3 4 2因为归根结底 await后面的普通函数实际上看成由Promise.resolve包裹后执行的
async function test() { console.log(1) await Promise.resolve(console.log(2)); console.log(3) } console.log(4) // 1 3 4 22、await后面是promise对象且promise未被决议才会产生阻塞
(如果是自己写的promise也会产生和Promise一样的效果)
请问下你是在哪里运行的?为什么控制台结果跟你的不一样?
== 0 == async function test() { await setTimeout(() =>console.log(111), 3000) console.log(222) }// 222 111 // 问题提出:为啥await没有起作用?await不是返回一个promise,当promise决议后才下一步吗? == 1 == async function test() { await new Promise(resolve => setTimeout(()=>console.log(111), 1000)) // 如果没有resolve 就不会往下执行 console.log(222)// 未执行 }// 111 == 2 == async function test() { await new Promise(resolve => setTimeout(()=>resolve(console.log(111)), 1000)) console.log(222) }// 111 222 正常版本 == 3 == async function test() { await Promise.resolve(setTimeout(()=>console.log(111),500)); console.log(222) }// 222 111 因为 await后面是一个已经resolve的promise 所以直接下一步 == 4 == async function test() { await Promise.all([setTimeout(()=>console.log(111),500)]); console.log(222) }// 222 111 当all的数组元素不是promise的时候 会调用Promise.resolve方法包装 产生与3一样结果 == 5 == async function test(){ await new _Promise(resolve=>setTimeout(()=>resolve(console.log(111)),1000)); console.log(222) }// 111 222 _Promise是自己模拟的promise由上,
1、await后跟的函数调用立即被调用(虽然听起来很像废话..async function test() { await setTimeout(()=>console.log(1),1000); await setTimeout(()=>console.log(2),500); await console.log(3) }// 3 2 1但并不代表执行全是同步的,请看下列代码的输出:
async function test() { console.log(1) await console.log(2) console.log(3) } console.log(4) // 1 3 4 2因为归根结底 await后面的普通函数实际上看成由Promise.resolve包裹后执行的
async function test() { console.log(1) await Promise.resolve(console.log(2)); console.log(3) } console.log(4) // 1 3 4 22、await后面是promise对象且promise未被决议才会产生阻塞
(如果是自己写的promise也会产生和Promise一样的效果)请问下你是在哪里运行的?为什么控制台结果跟你的不一样?
已修正,详见 #100 (comment) 感谢指正 💙
await-to-js 是个好东西
to不就是golang的错误处理吗?go自己的原生错误处理就被喷的不行,结果被js捡起来了...还成了好东西