# async await 机制

异步逻辑的处理一直是前端届老生常谈的问题,从回调函数到事件监听到es6的promise、generator,再到最后的异步解决方案终结者async、await,现在我们一步步解开async、await的魔法

在做异步请求时,我们通常用async await的方式,很神奇的可以将异步转为同步的写法:

function fetchData(params) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(2);
    }, 100)
  })
}

async function getData(params) {
  console.log('start');
  let d = await fetchData();
  console.log(d);
  console.log('end');
  return 'getData end'
}
getData().then(val => console.log(val))

打印结果依次是:start 2 end getData end

从这个例子,我们发现,在async函数内部,代码是一行一行执行的(同步执行),然后async函数会返回一个promise对象,并把最后的返回值作为此promise的resolve结果的值。

那么这种异步转同步的方式如何实现,我们如何来模拟实现这样一个async的功能的函数?

其实在,async之前,es6也推出了generator函数来辅助异步的操作,通过generator函数的控制流的方式,调用者可以决定何时让函数向下执行,先看一下generator如何使用:

function fetchData(params) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(2);
    }, 100)
  })
}

function *gen(params) {
  console.log('start');
  let a = yield 1;
  console.log(a);
  let b = yield fetchData();
  console.log(b);
  let c = yield 3;
  console.log(c);
  console.log('end');
  return 'getData end'
}

执行gen会返回一个迭代器

let g = gen()

执行迭代器的next方法,控制函数向下执行,每次执行会返回一个对象,包含value属性和done属性,其中value属性的值是yield语句后面的返回值,done属性表示该generator函数是否执行完毕,值为true或false

// 第一次执行next,执行的结果是打印一下 start 然后返回 {value: 1, done: false}
let first = g.next()

需要注意的是,第一次执行g.next()方法,虽然返回了 {value: 1, done: false},但是并没有将此返回值赋值给first变量。而是函数停止等号的右边,把执行的控制权交给了函数外部,直到下次执行next方法,才会继续回到函数内部继续向下执行,直到遇到了下个yield语句,依次类推。

现在我们再次执行next

let second = g.next() 
// 此时走到了这里 yield fetchData(); 返回的结果是 {value: Promise, done: false} 并赋值给 second

此时执行了console.log(a)这条语句了,但是打印出来的确实undefined,why?这是由于只有在下次执行next方法传入的参数才会赋值给变量a,而此时我们什么也没有传入,如果我们这样执行next:

let second = g.next('赋值给a的值')

那么此时打印的a的值就是'赋值给a的值',那如果我想把上个yield的结果赋值给a呢?很简单,这样做:

let second = g.next(first.value)

还没完,再执行一下yield,但是由于上次yield的value是个promise,我们需要等他的状态resolve后拿到了它的结果了再执行下次的yield,这样才能把结果传给b

second.value.then(data => {
  let third = g.next(data)
  // 此时data的值是2,执行 g.next(data),把值赋值给了b,所以打印出来的就是2

  // 执行了g.next(data)现在来到了执行 yield 3,返回 {value: 3, done: false},
  // 所以 third的值就是 {value: 3, done: false}
})

那么如果我下次执行next,这就比较麻烦了,因为我要拿到 third 的值,难道我要接着在上次在then内部执行?

second.value.then(data => {
  let third = g.next(data) // 执行到这里来到了 yield 3 返回 {value: 3, done: false}

  let four = g.next(third.value)
  // 此时 third.value 的值是3 赋值给了 c,打印的c的值就是 3
  // 执行 g.next(third.value),返回的结果是 {value: 'getData end', done: true} 由于后面没有了yield语句,所以done的值就是true,表示执行结束,并把最终返回值赋值给了value属性
})

实际上确实要这样做,但是和之前的yild后面返回的同步的值的做法就不一致了,那么如何才能保持同步的和异步的执行next过程保持一致呢?

为了达到一致的效果,我们可以把每次执行yild返回值的value属性的值使用promise包装一下,就是value的值实际是就转换成promise对象了,这样每次调用next方法就是这样的方式:

let first = g.next()
Promise.resolve(first.value).then(data => {
  let second = g.next(data)
  Promise.resolve(second.value).then(data => {
    let third = g.next(data)
    Promise.resolve(third.value).then(data => {
      let four = g.next(data)
      ...
      // 过程如何停止呢? 就是判断done属性是否为true了
    })
  })
})

这个看起来像callback hell的东西其实可以看成是generator函数的控制流过程。这个东西看着是不是相当熟悉?是的,之前我们在分析koa2的中间件的compose函数实际上跟这个过程很类似,只不过这里的g.next方法是自动执行的,而compose中的next中间件是交给用户手动执行的

那么现在我们要做的就是,实现一个函数来自动执行generator函数,我们想达到类似async函数一样的效果:

function runGenerator(genFn){
}

let fn = runGenerator(gen)

fn().then((result) => {})

runGenerator函数接收一个generator函数,执行完会返回一个新的函数,这个新函数会返回一个promise对象,以接收generator函数最终的返回值

分析到现在,现在要实现这个函数就是比较容易的事情了:

function runGenerator(generatorFn){
  return function(){
    return new Promise((resolve, reject) => {
      let g = generatorFn.call(this, ...arguments)
      function walk(data){
        let result = g.next(data);
        if(result.done){
          // 如果执行完毕,则返回
          return resolve(result.value)
        } else {
          return Promise.resolve(result.value).then(data => {
            walk(data)
          })
        }
      }
      walk()
    })
  }
}

简单分析一下这个函数,主要在内部使用了递归,判断done属性,如果执行完就返回最终的结果,否则就使用Promise.resolve包装一下result.value,并在then回调中再次执行next的过程。

现在我们来测试一下这个函数:

function *gen(params) {
  console.log('start');
  let a = yield 1;
  console.log(a, 'a======');
  let b = yield fetchData();
  console.log(b, 'b======');
  let c = yield 3;
  console.log(c, 'c======');
  let d = yield fetchData();
  console.log(d, 'd======');
  console.log('end');
  return 'getData end'
}

let fn = runGenerator(gen)
fn().then((data) => {
  console.log(data, 'data========')
})

打印的结果依次是:start、1、2、3、2、end、getData end,很棒。

其实这里还有点小瑕疵,就是没有处理错误的情况,比如执行yield语句抛错了,现在完善一下:

function runGenerator(generatorFn){
  return function(){
    return new Promise((resolve, reject) => {
      let g = generatorFn.call(this, ...arguments)
      function walk(data,type){
        let result = {}
        try {
          result = g[type](data); // type 有两种 next 和 throw
        } catch (error) {
          // 碰到 g.next() 报错或者g.throw() 执行,会走到catch,将错误抛出
          return reject(error)
        }
        if(result.done){
          // 如果执行完毕,则返回
          return resolve(result.value)
        } else {
          return Promise.resolve(result.value).then(data => {
            walk(data, 'next')
          }).catch(error => {
            // 异步的结果报错 执行g.throw(error)
            walk(error, 'throw')
          })
        }
      }
      walk(null, 'next')
    })
  }
}

# async

有了上面的对generator的认识,现在就来解答下async是什么,它和generator是什么关系?

到现在可以发现,我们实现的generator自执行函数runGenerator,它其实更像是个民间低配版本的async awat

熟悉async await应该知道,await执行结果返回的结果是promise,async函数执行最后返回的也是个promise函数

而我们的runGenerator已经完全满足了这个特性。只不过await 只接收返回resolve结果的promise,否则报错,而我们的函数会捕获yield报错语句,并将错误结果抛出。

所以,为什么说async函数只是promise的语法糖,它的底层实际使用的是generator + promise 的实现。实际上,在babel编译async函数的时候,也会转化成generatora函数,并使用自动执行器来执行它。

# generator 机制

现在我们明白了async是怎么回事了,它底层实际是基于generator的,但是这里又有个问题了,为什么每次使用yield,在generator内部使用都能知道执行到哪一步了呢?我们从一个例子来简单的了解一下

function *gen(){
  yield 1;
  yield 2;
  yield 3;
}
let g = gen()
console.log(g.next().value) // 1
console.log(g.next().value) // 2
console.log(g.next().value) // 3
console.log(g.next().value) // undefined

实际上,babel在编译这个函数的时候主要是做了两个事情:

  • 切割generator函数的yield代码
  • 生成一个变量用以保存generator函数的执行状态
  • 生成一个invoke方法,并绑定next方法,这个过程相当于创建了一个迭代器

首先,切割generator函数,切割后的会如下:

// 生成器函数根据yield语句将代码分割为switch-case块,后续通过切换_context.prev和_context.next来分别执行各个case
function gen$(_context) {
  while (1) {
    switch (_context.prev = _context.next) {
      case 0:
        _context.next = 2;
        return 'result1';

      case 2:
        _context.next = 4;
        return 'result2';

      case 4:
        _context.next = 6;
        return 'result3';

      case 6:
      case "end":
        return _context.stop();
    }
  }
}

然后,创建一个context变量:

var context = {
  next:0,
  prev: 0,
  done: false,
  stop: function stop () {
    this.done = true
  }
}

最后,创建一个invoke函数:

let gen = function() {
  return {
    next: function() {
      value = context.done ? undefined: gen$(context)
      done = context.done
      return {
        value,
        done
      }
    }
  }
}

测试一下

var g = gen()
g.next()  // {value: "result1", done: false}
g.next()  // {value: "result2", done: false}
g.next()  // {value: "result3", done: false}
g.next()  // {value: undefined, done: true}

这段代码不难理解,分析一下它的调用过程:

  • 调用gen(),返回g对象,包含next属性
  • 调用g.next(),会判断一下context的done属性,如果done为false,会执行一下gen$(context),
  • gen$(context)是个switch语句,根据context.prev值执行对应的case语句,并把结果返回,如果执行到最后没有yield语句,会把done属性置为true;
  • 最终g.next()方法返回一个对象,包含value属性和done属性,这个value属性值就是gen$(context)返回值,done属性值就是context的done属性值。

从中我们可以看出,「Generator实现的核心在于上下文的保存,函数并没有真的被挂起,每一次yield,其实都执行了一遍传入的生成器函数,只是在这个过程中间用了一个context对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行,看起来就像函数被挂起了一样」

babel在编译时做了比较多的复杂工作,感兴趣的同学可以在babel官网编译看看,但是核心的工作还是上面的流程。

# 总结

本篇文章就是大致讲解了一下async await的实现机制,并简单的探索了一下generator的yield的挂起原理,感兴趣的同学可以顺着这个思路再深挖一下。

有了对于generator函数的基本认识,下一篇文章就分析一下基于generator函数的redux-saga,看看它是如何处理异步请求的。

refer:

Babel将Generator编译成了什么样子 (opens new window)
Generator实现原理解析 (opens new window)
async 函数的含义和用法 (opens new window)

最后更新时间: 8/24/2020, 8:30:04 PM