async、await和generator函数内部原理

6,181 阅读4分钟

对async、await的理解,内部原理

async 是Generator函数的语法糖,并对Generator函数进行了改进

  1. async的实现就是将Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

spawn 函数就是自动执行器, 下面给出spawn函数的实现

function spawn(genF){
    return new Promise((resolve, reject)=>{
        const gen = genF() // 先将Generator函数执行下,拿到遍历器对象
        function step(nextF) {
            let next
            try {
                next = nextF()
            } catch(e){
                return reject(e)
            }
            if(next.done){
                return resolve(next.value)
            }
            Promise.resolve(next.value).then((v)=>{
                step(()=>{return gen.next(v)})
            }, (e)=>{
                step(()=>{return gen.throw(e)})
            })
        }
        step(()=> {return gen.next(undefinex)})
    })
}
  1. 更好的语义

async 和 await, 比起星号和yield,语义更加清楚,async表示函数里面有异步操作,await表示紧跟在后面的表达式需要等待结果。

  1. 更广的适用性

co模块约定,yield命令后面只能是Thunk函数或者Promise对象, 而async函数的await后面,可以是Promise和原始类型值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象, 查看spawn函数中Promise.resolve(next.value))

  1. 返回值是Promise

比Generator函数的返回值是Iterator对象方便,可以使用then方法指定下一步操作

你可能会问,async是Generator函数的语法糖,那什么是Generator函数呢?

Generator函数语法:重点是*和关键字yield

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();

执行Generator函数helloWorldGenerator的话,并不执行,而是返回一个迭代器对象,下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

yield 由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

Generator与协程

协程是一种程序运行的方式,可以用单线程实现,也可以用多线程实现。

  • 协程概念:

一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。

  • 协程与普通线程的差异:
  1. 普通线程是抢先式的,会争夺cpu资源,而协程是合作的,
  2. next 同一时间,可以有多个普通线程运行,而协程则只有一个在运行,其他协程则处在暂停状态。

Generator 函数是 ES6 对协程的实现,但是不完全,Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。

Generator 与上下文

Generator 函数, 它执行产生的上下文环境,一旦遇到yield命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行next命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。

Generator 的实现

具体请参考alloyteam的文章

js的Generator并非由引擎从底层提供额外的支持,而是通过代码的编写,来实现的函数暂停、按序执行。 在实现Generator过程中有两个关键点,一是要保存函数的上下文信息,二是实现一个完善的迭代方法,使得多个 yield 表达式按序执行,从而实现生成器的特性。 大体上就是使用由 switch case 组成的状态机模型中, 除此之外,利用闭包技巧,保存生成器函数上下文信息。

Regenerator 通过工具函数将生成器函数包装,为其添加如 next/return 等方法。同时也对返回的生成器对象进行包装,使得对 next 等方法的调用,最终进入由 switch case 组成的状态机模型中。除此之外,利用闭包技巧,保存生成器函数上下文信息。

上述过程与 C#中 yield 关键字的实现原理基本一致,都采用了编译转换思路,运用状态机模型,同时保存函数上下文信息,最终实现了新的 yield 关键字带来的新的语言特性。

这个链接可以查看转换后的代码:这个在线地址