async/await 原理


Generator

ES6 引入Generator,用于简化异步操作;

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

协程是一种比线程更轻量级的存在。普通线程是抢先式的,会争夺cpu资源,而协程是合作的,可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。它的运行流程大致如下:

  1. 协程A开始执行
  2. 协程A执行到某个阶段,进入暂停,执行权转移到协程B
  3. 协程B执行完成或暂停,将执行权交还给A
  4. 协程A恢复执行

协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它最大的优点,就是代码的写法非常像同步操作,如果去除 yield 命令,简直一模一样。如下:

function* gen(x) {
  console.log('start')
  const y = yield x * 2;
  const c = yield y * 2;
  return c;
}

const g = gen(1)
console.log(g.next())   // start { value: 2, done: false }
console.log(g.next(4))  // { value: 8, done: false }
console.log(g.next(1))  // { value: 1, done: true }

执行 gen(1)​ 时,gen()中的代码并不会执行,而是返回一个 ​Iterator ​对象。每次调用​ g.next() 时才会执行 gen() 中的代码,执行遇到 yield ​或 ​return​ ​时立即返回;

当遇到yield时,会执行yeild后面的表达式,并返回执行后的值,并进入暂停状态,此时done: false;遇到return时,会返回值,并结束,即done: true


g.next()的返回值永远都是{value: ... , done: ...}形式;

​next​() 函数可以接受参数,其参数会被赋给上个阶段异步任务的返回结果,即 yield 语句左侧的变量。


yield*语句

多个Generator相互包含时:

function* gen() {
  console.log('b1:')
  const y = yield 'b-1'
  console.log('b2:')
  const s = yield 'b-2'
  return s
}

function* test(){
   yield 'a-1'
   yield* gen(1)  //  gen(1) 中的 yield 会像 test 自己 yield 一样被逐个执行
   yield 'a-2'
}

let t = test(1)
console.log(t.next(1)); //    {value: "a-1", done: false}
console.log(t.next());  // b1:{value: "b-1", done: false}
console.log(t.next());  // b2:{value: "b-2", done: false}
console.log(t.next());  //    {value: "a-2", done: false}
console.log(t.next());  //    {value: undefined, done: true}


Generator中的this

Generator不是函数,更不是构造函数,其内部没有this构造函数返回的是this,而Generator返回的是一个Iterator对象;

function* G() {
  // this 为 undefine
}
const g = G()


执行器

把执行Generator(生成器)的代码封装成一个函数,这个函数通常被称为执行器,co 模块就是一个著名的执行器。

Generator 是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。两种方法可以做到这一点:

  1. 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
  2. Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。


例如:基于 Promise 对象的简单自动执行器:

// (执行器)自执行函数
function run(gen){
  var g = gen();

  function next(data){
    var result = g.next(data); 
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data); // 收到响应结果时,自执行,并传入响应结果
    });
  }
  next();
}

// 欲传入自执行函数中Generator
function* foo() {
    let response1 = yield fetch('http://localhost:8888') // fetch返回promise对象,yield命令将此返回结果返回至外部,并暂停执行当前协程,然后交出执行权。
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('http://localhost:8888') // fetch返回promise对象
    console.log('response2')
    console.log(response2)
}

// 调用自执行函数
run(foo);

如上,只要 Generator 函数还没执行到最后一步,next函数就调用自身,以此实现自动执行。通过使用Generator配合执行器,就能实现使用同步的方式写出异步代码了。


async/await

ES7 引入了 async/await,这种方式能够彻底告别执行器和生成器,实现更加直观简洁的代码。async通过异步执行并隐式返回 Promise 作为结果的函数。可以说async 是Generator函数的语法糖,并对Generator函数进行了改进。

前文中的代码,用async实现是这样:(即,async就是将 Generator 函数的星号(*)换成了async,将yield换成了await)

const foo = async () => {
    let response1 = await fetch('https://xxx')
    console.log('response1')
    console.log(response1)
    let response2 = await fetch('https://xxx')
    console.log('response2')
    console.log(response2)
}


async函数对 Generator 函数的改进,体现在以下四点:

  1. 内置执行器。Generator 函数的执行必须依靠执行器,而 async 函数自带执行器,无需手动执行 next() 方法。
  2. 更好的语义。async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  3. 更广的适用性。co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
  4. 返回值是 Promise。async 函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用 then() 方法进行调用。


async自带执行器,相当于把额外做的(写执行器/依赖co模块)都封装在内部,比如:

async function fn(args) {
  // ...
}

等同于:

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

// spawn就是自动执行器,其参数是一个Generator,相比之前简单版的执行器,async做了一些容错处理
function spawn(genF) { 
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let nextResult;
      // 容错处理:获取 yeild 返回结果时,若报错,则直接返回
      try {
        nextResult = nextF();
      } catch(e) {
        return reject(e);
      }
      // 若已完成,则返回 yeild 返回值中的 value 
      if(nextResult.done) {
        return resolve(nextResult.value);
      }
      
      // 统一将 yeild 返回结果中的 value 转为异步处理,因为 yeild 返回的 value 有可能是一个普通值,也有可能是一个Promise对象,为了使外部表现统一为异步,因此此处比统一将 yeild 返回值中的 value 处理成一个异步操作;
      // Promise.resolve(value)返回以给定值解析后的Promise对象,详见:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
      Promise.resolve(nextResult.value).then(function(v) { 
        step(function() { return gen.next(v); });    // 此处的 v 就是 next.value
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}


如上可知,async隐式返回 Promise , 所以await后的函数执行完毕时,await会产生一个微任务(Promise.then)。但这个微任务产生的时机是执行完await后,直接跳出async函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中。如下:

console.log('script start')

async function async1() {
  await async2()  // 此处会出现协程交换执行权,因此之后的代码会异步执行
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end')
}
async1()

setTimeout(function() {
   console.log('setTimeout')
}, 0)

new Promise(resolve => {
   console.log('Promise')
   resolve()
}).then(function() {
   console.log('promise1')
}).then(function() {
   console.log('promise2')
})

console.log('script end')
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout

分析这段代码:

  • 执行代码,输出script start
  • 执行async1(),会调用async2(),然后输出async2 end,此时将会保留async1函数的上下文,然后跳出async1函数。
  • 遇到setTimeout,产生一个宏任务
  • 执行Promise,输出Promise。遇到then,产生第一个微任务
  • 继续执行代码,输出script end
  • 代码逻辑执行完毕(当前宏任务执行完毕),开始执行当前宏任务产生的微任务队列,输出promise1,该微任务遇到then,产生一个新的微任务
  • 执行产生的微任务,输出promise2,当前微任务队列执行完毕。执行权回到async1
  • 执行await,实际上会产生一个promise返回,即
let promise_ = new Promise((resolve,reject){ resolve(undefined)})

执行完成,执行await后面的语句,输出async1 end

  • 最后,执行下一个宏任务,即执行setTimeout,输出setTimeout

注意

新版的chrome浏览器中不是如上打印的,因为chrome优化了,await变得更快了,输出为:

// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
复制代码

但是这种做法其实是违法了规范的,当然规范也是可以更改的,这是 V8 团队的一个 PR ,目前新版打印已经修改。 知乎上也有相关讨论,可以看看 www.zhihu.com/question/26…




举报

© 著作权归作者所有


1