ES6:async / await ---使用同步方式写异步代码

前言

最近博主在看异步编程的实现方法,从 Promise对象 到 Gerenator函数真的是头大,会想真的要写这么复杂的代码吗?
回答:当然不会。当我学到async和await的时候才知道原来 Promise对象 和 Gerenator函数都是为它做的铺垫。
博主建议如果你还不了解什么是异步编程可以去看看JavaScript异步编程的四种方法,看完以后可以看Promise对象Generator函数的异步应用
这篇文章会让你以同步的方式来写异步代码,真的很赞。

概述

在弄清楚什么是async和await之前,你需要先知道一个底层技术。我们需要讲解Generator的底层的实现机制---协程,带你一步一步来弄懂async/await的工作方式。

协程

协程是一种比线程更加轻量级的存在。不了解进程与线程的关系的可以先看这篇文章:进程与线程:形象而不抽象
协程与线程的关系就好像是线程与进程的关系,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。

  • 比如说,当前执行的是A协程,要启动B协程,那么A协程就需要将主线程的控制权交给B协程,这就体现在A协程暂停执行,B协程恢复执行;
  • 同样从B协程到A协程也是如此。通常,如果从A协程启动B协程,我们就把A协程称为B协程的父协程。
    协程其实不是被操作系统内核所管理的,而完全是由程序控制的。这样带来的好处是性能得到了很大的提升,不会被线程切换那样消耗资源。
    为了让你更好的理解协程是怎么执行的,我们结合一段代码来理解协程的规则:
function* genDemo() {
        console.log('开始执行第一段')
        yield 'generator 2'

        console.log('开始执行第二段')
        yield 'generator 2'

        console.log('开始执行第三段')
        yield 'generator 2'

        console.log("执行结束")
        return 'generator 2'
    }

1    console.log('main 0')

2    let gen = genDemo()
3    console.log(gen.next().value)
4    console.log('main 1')
5    console.log(gen.next().value)
6    console.log('main 2')
7    console.log(gen.next().value)
8    console.log('main 3')
9    console.log(gen.next().value)
10    console.log('main')
  • 对于上面的程序,只有一个父协程在主线程上执行,所以开始会打印第一行代码 main 0
  • 接着let gen = genDemo() 会创建一个gen协程,创建之后gen协程并没有立即执行。要让gen执行,需要调用gen.next。
  • 接着第三行代码中执行gen.next(),这时父协程暂定执行,协程切换到gen协程执行,所以打印“开始执行第一段”
  • 接着遇到yield关键字,gen协程停止执行,并把yield关键字后面的内容返回给父协程,父协程恢复执行... , 如果协程在执行期间,遇到return关键字,那么JavaScript引擎会结束当前协程,并把return关键字后面的内容返回给父协程。

小结

1.gen协程和父协程是在主线程上交互执行的,并不是并发执行的,它们之间切换是通过yield和gen.next来配合完成的。
2.当在gen协程中调用yield方法时,JavaScript引擎会保存gen协程的当前调用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行gen.next时,JavaScript引擎会保存父协程的调用栈信息,并恢复gen协程的调用栈信息。读到这里,相信你已经了解生成器(generator)协程是怎么工作的了吧。

async/await

为了使用更加直观简洁的代码来异步编程,就要学习async和await的工作原理。

async

根据MDN定义,async是一个通过异步执行隐式返回Promise作为结果的函数。
我们先看看它是怎么隐式返回Promise的,异步执行一会在解释。

async function foo(){
        return 2
    }
    console.log(fool()) // Promise{<resolved>:2}

执行这段代码,我们可以看到调用async声明的foo函数返回一个Promise对象,状态是resolved。

await

我们知道async函数返回的是一个Promise对象,那么下面我们再结合一段代码来解释await是什么。

1    async function foo() {
2        console.log(1)
3        let a = await 100
4        console.log(a)
5        console.log(2)
6    }
7    console.log(0)
8    foo()
9    console.log(3)

你能判断出打印出来的内容是什么吗?
我们先站在协程的视角来看看这段代码的整体执行情况:

  • 首先执行第7行代码打印迟来0,
  • 紧接着就是执行foo函数,由于foo函数是被saync标记过的,所以当进入该函数的时候,JavaScript引擎会保存当前的调用栈等信息,然后执行foo函数中的console.log(1)语句,并打印出1。
  • 紧接着就是执行foo函数中的await 100这个语句了,这里是我们分析的重点,因为在执行await 100这个语句时,JavaScript引擎会在背后为我们默默做了太多的事情,那么下面我们就把这个语句拆开,来看看那JavaScript到底做了什么事情。

当执行到await 100时,会默默创建一个Promise对象,代码如下:

let promise = new Promise((resolve,reject){
      resolve(100)
})
  • JavaScript引擎会把resolve(100)这个任务提交给微任务队列。如果你还不知道什么是微任务,请看宏任务与微任务
  • 然后JavaScript引擎会暂停当前协程的执行,将主线程的控制权交给父协程执行,同时会将promise对象返回给父协程。
  • 这时候父协程会调用promise.then来监控promise状态的变化。
  • 接下来继续执行父协程的流程,执行第9行代码,打印3。
  • 随后,父协程即将执行结束
  • 在结束之前,会进入微任务检查点,然后执行微任务队列,微任务队列中有resolve(100)的任务等待执行,执行到这里,会触发promise.then中的回调函数,回调函数被激活后,将主线程的控制权交给foo协程,并将value值传给foo函数协程,然后foo函数协程把value的值赋给变量a,然后foo协程继续执行后面的语句,执行完成以后,会把控制权归还给父协程。
    以上就是await/async的执行流程。正是因为async和await在背后为我们做了大量的工作,所以才可以能用同步的方式写出异步代码来。

总结

1.使用async和await可以实现用同步代码的风格来编写异步代码。这是因为async/awiat的基础技术使用了生成器和Promise。
2.另外v8引擎还为async/await做了大量的语法层面的包装。

思考题

留个代码供大家思考,你能分析出这段代码执行后输出的内容吗?

    async function foo() {
        console.log('fool')
    }
    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,reject){
        console.log('promise executor')
        resolve()
    }).then(function(){
        console.log('promise then')
    })
    console.log('script end')

欢迎在评论区分享你的答案。

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。
原文地址:https://www.cnblogs.com/XF-eng/p/14154464.html