[JS]带你深入async/await

552 阅读7分钟

最近想重新系统地整理一下前端的知识,因此写了这个专栏。我会尽量写一些业务相关的小技巧和前端知识中的重点内容,核心思想。

前言

作为一个前端工程师,相信大家都已经体验到了由ES7的async/await带来的好处了。可以说它彻底的改变了我们写代码的风格方式。今天我们就一起来深入一下,这个async/await的前世今生。

用法

async/await的用法相信大家都已经很熟了,这里就只作简单的描述。

  • async是一个关键字,作用是声明某个函数是async函数。
  • await必须写在async函数中。
  • async函数中的代码会逐行运行
  • await后可以跟一个promise实例,当promise的状态改变await才会结束。
function resolveAfter2Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('calling');
  const result = await resolveAfter2Seconds();
  console.log(result);
  // expected output: "resolved"
}

asyncCall();

意义

可以看出,async/await彻底解决了回调写法的问题。用同步的代码风格,实现回调的逻辑内容。让代码可读性更高。

而其实在此之前,我们在处理回调问题的过程中经历了很多个阶段。在最早的时候,如果希望有序地执行回调函数,就必须在回调中嵌套回调。如下代码:

var sayhello = function (name, callback) {
  setTimeout(function () {
    console.log(name);
    callback();
  }, 1000);
}
// 希望first second third 有序执行,就必须让他们在回调中嵌套。
sayhello("first", function () {
  sayhello("second", function () {
    sayhello("third", function () {
      console.log("end");
    });
  });
});

而es6中我们迎来了promise,他通过返回新的promise实例的方式。让我们可以不停的then,这样就解决了回调地狱的嵌套。参考如下:

var sayhello = function (name) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log(name);
      resolve();  //在异步操作执行完后执行 resolve() 函数
    }, 1000);
  });
}
sayhello("first").then(function () {
  return sayhello("second");  //仍然返回一个 Promise 对象
}).then(function () {
  return sayhello("third");
}).then(function () {
  console.log('end');
}).catch(function (err) {
  console.log(err);
})

但这样我们又掉进了另一个痛苦中,就是如果then的次数太多,同样会让代码的可读性越来越差。开发者希望追求的是一种,写起来像同步代码那样逻辑自然流程的代码风格。因此ES7的async/await彻底解决了这个问题。

var sayhello = function (name) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log(name);
      resolve();  //在异步操作执行完后执行 resolve() 函数
    }, 1000);
  });
}
var  main = async function(){
  try{
		await sayhello("first");
  	await sayhello("second");
  	await sayhello("third");
  	await sayhello('end');
  }catch(err){
    console.log(err);
  }
}
main();

还有多少人记得generator?

其实在上方描述的历史演变中,我们遗漏了ES6的generator了。generator是JavaScript对同步风格的一次尝试,也是async/await的原型。可以说async/await其实是generator的语法糖,在async/await出来之后,generator已经很少人用了。但在早期版本的cnpm和koa1中,我们都可以很容易找到generator的身影。

使用

function* 这种声明方式(function关键字后跟一个星号)会定义一个生成器函数 (generator function),它返回一个 Generator 对象。而在生成器函数中,我们可以使用yield语句。

function* gen() {
  yield 1
  yield 2
  yield 3
}
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next()) // { value: 2, done: false }
console.log(g.next()) // { value: 3, done: false }
console.log(g.next()) // { value: undefined, done: true }

我们发现生成器函数中的内容是可以暂停的,当函数中的代码运行到yield语句之后,就会暂停。只有当generator调用next之后,函数才会继续往下走。而generator每次调用next,都会返回一个对象,里面有由yield返回的value,和说明生成器函数是否执行到底了标识done。

如果希望在生成器函数执行到底之后返回的不是undefined,可以在函数底部return一个值。

function* gen() {
  yield 1
  yield 2
  return 3
}
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next()) // { value: 2, done: false }
console.log(g.next()) // { value: 3, done: true }

next传参

处理在generator调用next之后获得由生成器函数传递出来的值以外。在调用next时,也可以往生成器函数中传递值。写法参考以下代码:

function* gen() {
  var num1 = yield "from fun: 1";
  console.log(num1);
  var num2 = yield "from fun: 2";
  console.log(num2)
  return "from fun: 3"
}
const g = gen()
console.log(g.next("from outside 1")) // { value: "from fun: 1", done: false }
console.log(g.next("from outside 2")) // { value: "from fun: 2", done: false }
// "from outside 2"
console.log(g.next("from outside 3")) // { value: "from fun: 3", done: false }
// "from outside 3"

我们可以看到在yield语句返回的值就是next传进来的内容。同时要注意第一次调用next是传值没用的。从第二次开始调用next传参才用意义。

用generator实现async/await

相信看到这里,大家能发现generator跟async/await是有很多相似的地方的,如声明一个特殊的函数,在函数中可以用特殊的语句等。没错,async/await其实就是generator的语法糖。这时候也许会有大胆的朋友想问,我们能不能用generator实现async/await的功能呢?这其实也是一道面试题,我们今天就来一起尝试一下吧。

目标

我们先来确认一下,我们想要的效果是怎样的。假如现在有一个async/await代码

async function asyncFn() {
  const num1 = await fn(1)
  console.log(num1)
  const num2 = await fn(num1)
  console.log(num2) 
  const num3 = await fn(num2)
  console.log(num3) 
  return num3
}
const asyncRes = asyncFn()
asyncRes.then(res => console.log(res)) 

那用generator写法应该希望是

function* gen() {
  const num1 = yield fn(1)
  console.log(num1) 
  const num2 = yield fn(num1)
  console.log(num2)
  const num3 = yield fn(num2)
  console.log(num3) 
  return num3
}

const asyncRes = gen()
asyncRes.then(res => console.log(res)) 

但我们知道generator必须执行到底才会有具体的返回的,而async是执行后就会返回一个promise实例。所有我们肯定是要再套一层函数作处理的,一般这种函数叫高阶函数。

function* gen() {
  const num1 = yield fn(1)
  console.log(num1) 
  const num2 = yield fn(num1)
  console.log(num2)
  const num3 = yield fn(num2)
  console.log(num3) 
  return num3
}
// genToAsync返回一个函数,作用就是执行过这个函数之后,gen就变成async函数了,是一个语义。
const genToAsync = generatorToAsync(gen)
// asyncRes得到一个promise实例
const asyncRes = genToAsync()
asyncRes.then(res => console.log(res)) 

generatorToAsync

经过上方提示,我们已经很明确generatorToAsync需要做到以下几点:

  • 执行后应该是返回一个函数asyncFunc,这个函数就是表明,我们已经把generator转成async函数了的意思。
  • asyncFunc执行后应该返回一个promise实例
  • promise的状态根据asyncFunc函数中代码的执行有没有报错决定。同时如果有return值,promise的结果就是return的值。
function generatorToAsync(genFun){
  // 返回asyncFunc函数
  return function(){
    // 先执行一次genFun
  	const gen = genFun.apply(this, arguments);
    // 返回一个promise对象
    return new Promise((resolve, reject) => {
      // 定义一个go函数,作用是可以控制执行gen的不同函数,这里会用到next和throw
      function go(key, arg) {
        let res
        try {
          // 这里有可能会执行返回reject状态的Promise
          res = gen[key](arg) 
        } catch (error) {
          // 如果执行过程中报错,直接reject
          return reject(error) 
        }
        // gen执行完next之后的返回是一个对象,我们需要把done和value拿到。
        const { value, done } = res
        if (done) {
          // 如果done为true,说明走完了,进行resolve(value)
          return resolve(value)
        } else {
          // 如果done为false,说明没走完,还得继续走
          // value有可能是:常量,Promise,Promise有可能是成功或者失败
          // promise执行返回的值 通过next传给gen内部
          return Promise.resolve(value).then(val => go('next', val), err => go('throw', err))
        }
      }
      // 执行go
      go("next")
    })
  }
}

底层概念

这里简单提一个v8实现async/await或者generator的机制,其中最重要的一个概念就是【协程】。协程是在线程下的一个运行机制,一个线程可以有多个协程,每个协程拥有自己的寄存器上下文和栈。但每次只能有一个协程运行。所有当代码运行到yield之后,gen函数中的协程就会把执行权交到外部。外部代码就会继续运行,当运行到next调用之后,协程又会交给gen函数内部。

总结

async/await是很常用的语法,但少人用会去了解他背后的原理。今天带大家再次了解了js在回调问题上的处理历史,并带大家回顾了生成器的使用。同时我们用生成器实现了async/await的功能。希望对大家有所帮助。

参考

developer.mozilla.org/zh-CN/docs/…

developer.mozilla.org/zh-CN/docs/…

juejin.cn/post/700703…