前言: 上一篇文章JS异步大法总结篇(二):从回调到Promise,摆脱磨人的回调小妖精了解了callback回调函数和Promsie(),这小结内容中我们来看看Generator生成器,以及Promsie的升级版async/await这两对“基佬”兄弟。
本文阅读时长大概15min。
先来看一道异步的经典面试题,看看它会输出什么?
async function foo() {
console.log('foo')
}
async function bar() {
console.log('bar start')
await foo()
console.log('bar end')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
bar();
new Promise(function (resolve) {
console.log('promise executor')
resolve();
}).then(function () {
console.log('promise then')
})
console.log('script end')
答案文末揭晓哦,接下来让我们来看看Generator(生成器)和async/await吧。
1.Generator(生成器)
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。 首先让我们来看看什么是生成器,整代码:
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
console.log(hw.next().value) //hello
console.log(hw.next().value) //world
console.log(hw.next().value) //ending
console.log(hw.next().value) //undefined
- 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
- 外部函数可以通过 next 方法恢复函数的执行 一句话总结就是:通过yield来暂停执行,通过next()循环执行。
这样一来语义化就十分明显了,虽然生成器已经能很好地满足我们的需求了,但是程序员的追求是无止境的,这不又在 ES7 中引入了 async/await,这种方式能够彻底告别执行器和生成器,实现更加直观简洁的代码。
关于Generator 函数的语法请移步阮大神的es6教程Generator 函数的语法
2.async/await:使用异步的的方法来写同步的代码
async/await的出现不能说明Promise就是不好,相反Promise在异步界地位很高。async/await的出现是为了 Promsie服务的,这两个“基友”是Promise的进化版。 首先要注意两点:
- async必须声明的是一个function
const demo = async function(){} //正确
const async demo = function () {} // 错误
- await是在async的函数内部使用,必须是直系(作用域链不能隔代)
let data = 'data'
demo = async function () {
const test = function () {
await data
}
}
报错:Uncaught SyntaxError: await is only valid in async function。
2.1 async/await的本质
2.1.1 async的本质:返回一个Promise对象。举个栗子:
(async function () {
return '我是Promise'
//等价于 return Promise.resolve('我是Promise')
//等价于 return new Promise((resolve,reject)=>{resolve('我是Promise')})
})()
// 返回是Promise
//Promise {<resolved>: "我是Promise"}
从上面这个栗子可以看出:async的本质就是一个Promise对象喽,那么既然是Promise对象就还是可以像Promise一样使用function的写法。
const func = async ()=>{
return Promise.resolve('我是Promise')
}
func()
.then(result=>{
console.log(result)
})
.catch()
2.1.2 await的本质:提供等同于”同步效果“的等待异步返回能力的语法糖。
const demo = async ()=>{
let result = await new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('我延迟了一秒')
}, 1000)
});
console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
return result //注:这里必须要返回值,与Promise要有返回值是一样的道理
}
// demo的返回当做Promise
demo()
.then(result=>{
console.log('输出',result);
})
以上代码会输出: 输出:我延迟了一秒 我由于上面的程序还没执行完,先不执行“等待一会
只要await返回的值,都一定会等待它执行完毕之后。是这样吗?我们再看一个栗子:
const demo = async ()=>{
let result = await setTimeout(()=>{
console.log('我延迟了一秒'); //任务3
}, 1000)
console.log('我由于上面的程序还没执行完,先不执行“等待一会”'); //任务1
return result
}
demo().then(result=>{
console.log('输出',result); //任务2
})
以上代码会输出: 我由于上面的程序还没执行完,先不执行“等待一会” 输出 1 我延迟了一秒
why???其实原因就是在于setTimeout()这个延迟函数了。之前提到过setTimeout()返回的是一个计时器的id,所以return 1。另外,setTimeout()函数内的代码是会放到一个延迟队列当中。任务的执行顺序是:消息队列中的宏任务->微任务队列中的任务->延迟队列中的任务。所以先执行微任务队列中的两个任务(任务1,任务2),再执行延迟队列中的任务3。所有就是这个理!
2.2 async/await登上历史舞台
我们再看看上一小节的栗子:
const setDelay = (time) => {
return new Promise((resolve, reject) => {
if (typeof time != 'number' || time > 5) reject(new Error('输入的time有误,请重新输入!'))
setTimeout(() => {
resolve(`先是执行setDelay() 共执行了${time}秒哦`)
}, time * 1000)
})
}
const setDelaySeconds = (seconds) => {
return new Promise((resolve, reject) => {
if (typeof seconds != 'number' || seconds > 5000) reject(new Error('输入的time有误,请重新输入!'))
setTimeout(() => {
resolve(`先是执行了setDelaySeconds() 共执行了${seconds}毫秒哦 `)
}, seconds)
})
}
假设要执行这样一组任务要执行[setDelay(2),setDelay(3),setDelaySeconds(2000),setDelaySeconds(3000)] 如果用Promise链式的写法,就有点恶心了是吧,then、then、then,then的头晕。 那么async/await这对好基友是这么解决的呢?整代码:
const demo = async() => {
console.log('开始执行');
console.log(await setDelay(2));
console.log(await setDelay(3));
console.log(await setDelaySeconds(2000));
console.log(await setDelaySeconds(3000));
console.log('完成啦');
}
demo()
简简单单,是不是感觉人生一下子达到了高潮。。。 使用立即执行函数也可以:
(async()=>{
//任务代码
})
2.3 async/await的错误处理
写法是简单方便多了,那 async/await这两个基佬是如何处理错误的呢?
- 使用链式写法捕获错误(推荐写法)
const demo = async() => {
//执行任务
}
.catch(err){
console.log(err)
}
- 使用try/catch来捕获错误
const demo = async() => {
try{
//执行任务
}
catch(err){
console.log(err)
}
}
demo()
2.4 async/await的中断(终止程序)
首先我们要明确的是,Promise本身是无法中止的,Promise本身只是一个状态机,存储三个状态(pending,resolved,rejected),一旦发出请求了,必须闭环,无法取消,之前处于pending状态只是一个挂起请求的状态,并不是取消,一般不会让这种情况发生,只是用来临时中止链式的进行。
不同于Promise的链式写法,写在async/await中想要中断程序就很简单了,因为语义化非常明显,其实就和一般的function写法一样,想要中断的时候,直接return一个值就行,null,空,false都是可以的。举个栗子:
const demo = async() => {
console.log('开始执行');
console.log(await setDelay(2));
console.log(await setDelay(3));
return '我退出了,下面的不进行了';
// 以下写法都可以
// return;
// return false;
// return null;
console.log(await setDelaySeconds(2000));
console.log(await setDelaySeconds(3000));
console.log('完成啦');
}
demo()
.catch(err=>{
console.log(err)
})
总结:本文简单介绍了一下Generator(生成器),接下来介绍了async/await。介绍了两个语法糖的本质,并从写法、中断处理与Promise进行了对比。来看看开头的问题吧。
async function foo() {
console.log('foo') //3
}
async function bar() {
console.log('bar start') //2
await foo()
console.log('bar end') //6
}
console.log('script start') //1
setTimeout(function () {
console.log('setTimeout') //8
}, 0)
bar();
new Promise(function (resolve) {
console.log('promise executor') //4
resolve();
}).then(function () {
console.log('promise then') //7
})
console.log('script end') //5
代码输出的顺序如上,有了前面的知识基础,相信大家对结果都没什么问题。对以下几处再稍作解释。 任务6/7是位于微任务队列中,任务8是处于延迟队列当中。当同步代码执行完任务5之后,接下来去执行微任务队列中的任务6/7,最后去执行延迟队列中的任务8。
以上三篇小文章是对异步方法的一个大致总结。本人前端菜鸟一枚,可能有一些对方总结不到位或者存在问题,欢迎大家批评斧正,也希望与大家一起交流学习,共同进步。