JavaScript异步编程:async底层原理与promise/generator的关系

同系列文章: JavaScript异步编程:async/await

快速回顾 promise 与 generator

我们都知道,async/await 只是 promise 的语法糖🍬。

在进入正文前,让我们来快速回顾一下 promise 和 generator。

Promise

promise 的出现就是为了解决回调地狱。在 promise 出现以前,为了拿到异步事件处理结果从而开展后续工作,需要使用回调函数。当我们需要串行执行很多有依赖关系的异步事件时,就会形成回调地狱:

promise 相当于给了我们一个异步事件结果的承诺,允许我们后续添加处理函数。将以上代码用 promise 可以改写为:

js 复制代码
loadScript("1.js")
  .then(function(script) {
    return loadScript("2.js");
  })
  .then(function(script) {
    return loadScript("3.js");
  })
  .then(function(script) {
    // ...
  })
  .catch(function(error) {
    // ...
  })

是不是优雅了很多!

promise 初始状态是 pending,如果 resolve(value) 执行,则变为 fulfilled;如果 reject(error) 执行,则变为 rejected

执行函数中 resolvereject 只会执行一个,其余的会被忽略。

js 复制代码
new Promise((resolve, reject) => {
  setTimeout(() => resolve("value"), 2000);
})
  .finally(() => alert("Promise ready")) // triggers first
  .then(result => alert(result)); // <-- .then shows "value"

promise 的链式调用中,每个 then 都可以返回一个 promise 或 thenable 对象(有 .then 方法的任意对象),继续传递给下一个 then,而 finally 并不参与结果的传递,返回的任何结果都会被忽略,此前的 promise 结果将会穿过 finally 直接向下传递。在链式调用中,只需要在最后添加一个 catch 来捕获错误,之后可以继续添加 then 来处理。未被 catch 捕获的错误可以通过监听全局事件 unhandledrejection 捕获。

一个 settled promise 状态将不会再发生改变,并且 promise 是无法取消的。

Promise API:

  • Promise.all:并行处理多个 promise,只要有一个被 reject 了,则整个 promise 立刻被 reject,其他结果将会被忽略。
  • Promise.allSettled:并行处理多个 promise,不论结果如何都会等待所有 promise,返回结果对于成功的则为 {status:"fulfilled", value:result} ;失败的则为 {status:"rejected", reason:error}
  • Promise.race:返回第一个结果,无论成功与否。
  • Promise.any:返回第一个成功结果。
  • Promise.resolve/reject:返回一个 resolved / rejected 的结果。

Generator

调用 generator function 可以创建一个generator 对象,通过 yield 关键词可以让函数返回多个结果。每次调用 generator 的 next 方法,都会执行到最近的 yield <value>,然后函数执行暂停,value 被返回。调用 next 方法返回一个形如 {value: any, done: false} 的对象,最后一个 yield 执行完毕后 done 将变为 true。调用已结束的 generator 只会返回 {value: undefined, done: true}

js 复制代码
function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

// "generator function" creates "generator object"
let generator = generateSequence();
alert(generator); // [object Generator]
let one = generator.next(); // {value: 1, done: false}
let two = generator.next(); // {value: 2, done: false}
let three = generator.next(); // {value: 3, done: true}

可以遍历 generator 直至 donetrue

js 复制代码
function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1, then 2
}

let sequence = [...generateSequence()]; // 1, 2

generator 也可以进行组合代理,使用 yield* 将另一个 generator 嵌入进来。

js 复制代码
function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generatePasswordCodes() {
  yield* generateSequence(48, 57); // 0..9
  yield* generateSequence(65, 90); // A..Z
  yield* generateSequence(97, 122); // a..z
}

let str = '';

for(let code of generatePasswordCodes()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z

yield 是个双向通道,既可以向外返回值,也可以向内输送值。从第二个 next() 起,可以传递参数到 generator 内部。

js 复制代码
function* gen() {
  let ask1 = yield "2 + 2 = ?";
  alert(ask1); // 4
  let ask2 = yield "3 * 3 = ?"
  alert(ask2); // 9
}

let generator = gen();
alert( generator.next().value ); // "2 + 2 = ?"
alert( generator.next(4).value ); // "3 * 3 = ?"
alert( generator.next(9).done ); // true

generator 也可以支持异步。

js 复制代码
async function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 1000));
    yield i;
  }
}

(async () => {
  let generator = generateSequence(1, 5);
  for await (let value of generator) {
    alert(value); // 1, then 2, then 3, then 4, then 5 (with delay between)
  }
})();

next 调用方法:let result = await generator.next()

其他 API:

  • generator.throw:抛出一个错误,可以在函数内通过 try/catch 捕获,也可以在函数外捕获。
  • generator.return(value):强行结束 generator,返回 {value: 传入的value, done: true}

Generator + Promise = async/await?

先来看一个例子,假设我们可以通过 foo 来根据参数获取一些特定的数据,返回一个 promise:

js 复制代码
function foo(x,y) {
  return request("http://some.url.1/?x=" + x + "&y=" + y);
}

按照一般的方式,我们通常会这样调用:

js 复制代码
foo(11, 31)
  .then(
    function(text){
      console.log( text );
    },
    function(err){
      console.error( err );
    }
  );

现在把它放到一个 generator function 里:

js 复制代码
function *main() {
  try {
    var text = yield foo(11, 31);
    console.log(text);
  } catch (err) {
    console.error(err);
  }
}

已知 foo(11, 31) 返回的是一个 promise,如果想要成功拿到它的值,根据 generator 的特性,就需要这么做:

js 复制代码
var it = main();
var p = it.next().value;
p.then(
  function(text){
    it.next(text);
  },
  function(err){
    it.throw(err);
  }
);

看下发生了什么?

  1. 在 generator function 里 yield 一个 promise;
  2. 调用 next 拿到这个 promise;
  3. 等待 promise 完成,并在 then 里拿到最终的结果;
  4. 再次调用 next 并通过传参将 promise 的结果传回 generator function,此时 text 就会拿到想要的结果;
  5. 如果发生了错误就在外面 throw 回去,并被函数里面的 catch 捕获。

是不是有点眼熟?这个过程正好就是 async/await 做的事!

js 复制代码
async function main() { 
  try { 
    var text = await foo(11, 31); 
    console.log( text ); 
  } catch (err) { 
    console.error( err ); 
  } 
}

更进一步,我们可以把以上的手动操作通过一个 helper 函数自动化。毕竟,如果每次 yield 一个 promise 都需要在外面监听 promise 的最终结果,再回传给函数内部,还是挺繁琐的。

在下面的代码中,run 接收一个 generator function 作为参数,并自动执行里面的任务。

js 复制代码
function run(gen) {
  var args = [].slice.call(arguments, 1); // 收集除 gen 以外的额外传递的参数
  var it = gen.apply(this, args); // 调用 generator function 得到 generator 对象
  return Promise.resolve().then(function handleNext(value){ // 确保返回的是promise
    var next = it.next(value); // 把值传递回 generator function 内部,拿到下一次需要处理的值
    return (function handleResult(next){
      if (next.done) { // 如果所有值都 yield 完毕,返回最终的值
        return next.value;
      } else { // 如果 yield 没有执行完,则继续迭代
        return Promise.resolve(next.value)
          .then(handleNext, function handleErr(err) { // 如果 promise 成功,则继续处理,并把值返回给 generator function;如果失败,则把错误扔回 generator function 内部,然后继续处理剩下的
            return Promise.resolve(it.throw(err)).then(handleResult);
        });
      }
    })(next);
  });
}

这个自动执行 generator funtion 的 helper 函数的执行流程如下:

  1. 调用传入的 generator function 得到 generator 对象。
  2. 接下来将循环处理所有 yield 的值。
  3. 第一次调用 next 方法,传入的 value 被忽略,拿到结果。
  4. 如果 donetrue,说明已经处理完所有值,直接返回最终值。
  5. 否则等待该值(promise)的结果,如果 promise 被 resolve,则将成功值通过 next 传回 generator function 内部,并针对 next 的返回值开启新一轮的处理(第 4 步);如果 promise 被 reject,则将错误抛回 generator function,然后继续处理接下来的值。

写一个小例子来测试一下:

js 复制代码
function asyncTask() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000)
  });
}
function* gen() {
  let result = yield asyncTask();
  return result;
}

// async/await
async function run1() {
  let result = await asyncTask();
  console.log(result); // ---> test 1: expected to be 1
}
run1();

// promise + generator
run(gen).then(v => console.log(v)); // ---> test 2: expected to be 1

直接使用 async/await 和使用 promise + generator 的方式得到了相同的结果。

延伸:事件循环 Event loop

在事件循环模型中,JS 引擎执行的任务可以分为宏任务和微任务。在每个宏任务结束后,在执行下一个宏任务之前,JS 引擎都会扫描一下当前的微任务队列中是否有微任务,如果有的话就会执行这些微任务。promise 的处理函数.then / .catch / .finally都是微任务。

以下是一个常见的面试题,考察代码的执行顺序:

js 复制代码
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
  setTimeout(() => {
    console.log('timer1')
  }, 0)
}

async function async2() {
  setTimeout(() => {
    console.log('timer2')
  }, 0)
  console.log("async2");
}

async1();

setTimeout(() => {
  console.log('timer3')
}, 0)

console.log("start")

可以先试着自己回答一下。

了解了 async/await 的内部机制后,就很容易作答了。await 之前是同步代码,自然会按顺序执行,紧跟在 await 后面的函数也会立刻执行(想象 yield 后跟一个产生 promise 的函数,会立即执行,只是返回的 promise 的状态是 pending)。接着函数被暂停,所以函数外的同步代码会继续执行。而由于需要等待 promise 变为 settled 之后,才会回到函数内部继续执行,恢复函数执行的动作发生在 promise 的 then/catch 的处理函数内,这一步是异步的,会被放在微任务队列中,所以会等待所有函数外部的同步代码都执行完,才会恢复函数内部的执行。

下面揭晓答案,你答对了吗?

text 复制代码
async1 start
async2
start
async1 end
timer2
timer3
timer1

参考资料

  1. Promises, async/await
  2. Generators, advanced iteration
  3. You Don't Know JS: Async & Performance - Generators
相关推荐
SameX9 分钟前
初识 HarmonyOS Next 的分布式管理:设备发现与认证
前端·harmonyos
M_emory_36 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito39 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
fighting ~1 小时前
react17安装html-react-parser运行报错记录
javascript·react.js·html
老码沉思录1 小时前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js
abments1 小时前
JavaScript逆向爬虫教程-------基础篇之常用的编码与加密介绍(python和js实现)
javascript·爬虫·python
mon_star°2 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184552 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
老码沉思录2 小时前
React Native 全栈开发实战班 - 状态管理入门(Context API)
javascript·react native·react.js