JS异步大法总结篇(三):async/await异步口味全新升级

665 阅读7分钟

前言: 上一篇文章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。

以上三篇小文章是对异步方法的一个大致总结。本人前端菜鸟一枚,可能有一些对方总结不到位或者存在问题,欢迎大家批评斧正,也希望与大家一起交流学习,共同进步。