1.概述

        JavaScript的async/await尝尝被描述为“写起来像同步、执行是异步”,这种体验的背后是通过Generator+Promise+自动调度器(状态机)来进行实现的。

2.深入理解

2.1 async/await本质是什么?

在JavaScript中:

  • async function会被编译为一个Generator函数
  • 每一个await会被编译为yield一个Promise(Generator及其 yield 本质上是完全同步的,异步行为来源于Promise)
  • 由一个隐藏的执行器(runtime或babel)不断调用.next()来驱动Generator继续执行
    async/await 的语法糖
    = Generator 的语法增强
    + Promise 异步能力
    + 自动状态机调度
    

示例:

async function foo() {
  const a = await delay(1000, 'a')
  const b = await delay(1000, 'b')
  return a + b
}

相当于:
function* foo() {
  const a = yield delay(1000, 'a')
  const b = yield delay(1000, 'b')
  return a + b
}

        所以现在问题就在,JavaScript如何定义一个执行器来不断.next()恢复这个Generator(next的作用是执行从当前到下一个yield处并返回{ value: Promise/其他, done: boolean })

2.2 从Generator角度理解async/await

        一个Generator函数执行后不会立刻运行,而是返回一个“暂停”的迭代器

const gen = foo();
gen.next();     // 开始执行
gen.next(v);    // 将上一个 yield 的结果传回去
gen.next(v);    // 返回完整结果

        如果一个 Generator 中有N个yield,就会产生N+1个执行阶段

start  →  yield1   →  yield2   → return (done)

        这也就是为什么,有两个await,会触发三次next()。在这个过程中,yield了一个Promise的时候,执行器看到yield了一个 Promise,然后等待Promise.resolve,然后再调用generator.next(resolvedValue),此时这个等待过程才是异步的。

2.3 Promie与await的关系

const a = await delay(...);

编译为:
const a = yield delay(...);

        说明以下几点:

  • await后必须跟一个Promise
  • yield的值必须让执行器等待Promise resolve
  • resolve的结果作为下一次next(value)的形参传回Generator

3.手写async/await执行器

        利用Generator+Promise,将yield Promise的逻辑自动化

<script>
  function run(generationFn) {
    // 获取 generator 对象(初始暂停态,未开始执行函数体)
    const gen = generationFn()

    function step(nextFn, value) {
      let next
      try {
        console.log('value=', value);
        // 推进下一步,实际上执行的是传进来的 gen.next
        // 遇到 yield 表达式,返回一个 { value: Promise /* delay(...) */, done: boolean } 对象
        // 函数体里面有 n ci yield,那么就 next n+1 次,函数开始到第一个 yield/yield 之间的切换/最后 return
        /*
        比如执行完第一轮,获取到 a 的 Promise,然后在 Promise.resolve 那里解析完拿到结果'a',在第二轮这里传进来
        第二轮传进来的时候执行到 nextFn,此时做了2件件事:1.将'a'赋值给a 2.执行下一行代码直到下一个 yield
        */
        next = nextFn(value)
        console.log('next=', next);
      } catch (e) {
        return Promise.reject(e)
      }
      // 若 next.done 为true,则说明执行完成
      if (next.done) {
        return Promise.resolve(next.value)
      }
      // 等待异步函数的结果,再继续执行(next.value 为 Promise)
      return Promise.resolve(next.value)
        .then(
          v => step(gen.next.bind(gen), v),
          e => step(gen.throw.bind(gen), e))
    }

    // 开始执行,逐步推进整个状态机(第一次未传入,value 为 undefined)
    return step(gen.next.bind(gen))
  }

  // 模拟异步状态,返回 Promise 对象
  function delay(ms, value) {
    return new Promise(resolve => {
      setTimeout(() => resolve(value), ms);
    });
  }

  // start  -- next() -->   yield #1   -- next(v) -->   yield #2   -- next(v) -->   return
  function testAsync() {
    return run(function* () {
      const a = yield delay(1000, 'a')
      const b = yield delay(1000, 'b')
      return a + b
    })
  }

  testAsync().then(console.log)
</script>

第一次进入next,value为undefined:

运行结果:

核心逻辑:

  • 遇到yield自动暂停
  • 等Promise.resolve完成后继续next
  • 将resolve的值作为next(value)传入Generator
  • 循环完成,直到return

4.状态机视角

        无论是V8引擎(支持async/await)还是Babel(把async/await 转成generator+run函数),最终都会把async函数转换成一个显式的状态机,结构大概如下:

state 0: 执行到第一个 await(yield)
state 1: 从 await 恢复,继续执行到第二个 await
state 2: 从 await 恢复,执行 return

        await并非是阻塞(不阻塞全局线程,而是暂停async函数的上下文,await后续的函数体逻辑会被拆分为微任务,在Promise.resolve后执行),只是让当前函数“暂停”并等待Promise结果,由执行器保证暂停后的恢复顺序,这也就是为什么JS单线程模型能同时保证异步能力与同步书写的基础

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐