javaScript实现 async 函数

挑战介绍

本节我们来挑战一道大厂面试真题 —— 实现 async 函数。

挑战内容
请实现一个 myAsync 函数,这个函数用来模拟 async 函数的功能,最终能通过下面的测试代码即可:

function fn() {
  return myAsync(function* () {
    yield 1;
    yield 2;
    return 3;
  });
}

const p = fn();
p.then((val) => {
  console.log(val); // 打印 3
});

提示
async 函数其实就是 Generator 函数的语法糖,它的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里,就是本题要实现的 myAsync 函数。

这个 myAsync 函数接收一个 Generator 回调函数作为参数,在 myAsync 函数中执行 Generator 函数并自动执行 next 方法,最终返回一个 Promise 实例,把状态为 done 的值 resolve 出来,把错误的信息 reject 出来。

知识点

Generator 函数

Generator 函数是 ES6 提供的一种异步编程解决方案,它是可以用来控制迭代器的函数,并且语法与传统的函数完全不同,我们来看下面这个示例:

function printNum() {
  console.log(1);
  console.log(2);
  console.log(3);
}

printNum(); // 程序最终依次输出 1,2,3

这是一个正常的函数,如果我们把这个函数改造成 Generator 函数,代码如下:

function* printNum() {
  yield console.log(1);
  yield console.log(2);
  yield console.log(3);
}

printNum(); // 这样执行不会有任何反应

此时执行 printNum,不会有任何反应,加上了 yield 关键字后,程序中的打印逻辑都被中断了。

我们需要调用函数返回值的 next 方法,才会生效,代码如下:

function* printNum() {
  yield console.log(1);
  yield console.log(2);
  yield console.log(3);
}

fn = printNum();
fn.next(); // 打印 1
fn.next(); // 打印 2
fn.next(); // 打印 3

这样,程序的执行就会变得可控,它们可以暂停,然后在需要的时候恢复,小结一下:

  1. Generator 函数比普通函数多一个 *。
  2. 函数内部用 yield 来控制暂停代码的执行。
  3. 函数的返回值通过调用 next 来恢复代码的执行。

协程

传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做"协程"(coroutine),意思是多个线程互相协作,完成异步任务。

协程有点像函数,又有点像线程。它的运行流程大致如下:

第一步,协程 A 开始执行。

第二步,协程 A 执行到一半,进入暂停,执行权转移到协程 B。

第三步,(一段时间后)协程 B 交还执行权。

第四步,协程 A 恢复执行。

上面流程的协程 A,就是异步任务,因为它分成两段(或多段)执行。

举例来说,读取文件的协程写法如下。

我们以实际的代码来举例:

function* A() {
  console.log("A");
  yield B(); // 暂停 A,执行 B
  console.log("end");
}
function B() {
  console.log("B");
  return 1; // B 执行完了,返回,继续执行 A
}
let gen = A();
gen.next();
gen.next();

// A
// B
// end

上面代码的函数 A 是一个协程,它的奥妙就在其中的 yield 命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield 命令是异步两个阶段的分界线。

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

async 函数的实现原理
我们知道了 Generator 函数的用法,现在用它来处理异步,代码如下:

const fs = require("fs");

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function (error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const genFn = function* () {
  const a = yield readFile("a.json");
  const b = yield readFile("b.json");
  console.log(JSON.parse(a));
  console.log(JSON.parse(b));
};

上面代码的函数 genFn 可以写成 async 函数,就是下面这样。

const asyncReadFile = async function () {
  const a = await readFile("a.json");
  const b = await readFile("b.json");
  console.log(JSON.parse(a));
  console.log(JSON.parse(b));
};

一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。

但是 Generator 函数的执行,每一步都要执行 next 方法,非常不方便,能不能让它一次性执行完毕呢?

上文中的 genFn 方法,我们让他执行完,代码如下:

let g = genFn();
// next返回值中有一个 value 值,这个 value 是 yield 后面的结果
g.next().value((err, data1) => {
  g.next(data1).value((err, data2) => {
    g.next(data2);
  });
});

注意这里的 value 值,是调用 next 方法生成的,比如:

function* printNum() {
  yield 1;
  yield 2;
  return 3;
}

fn = printNum();
console.log(fn.next()); // {value: 1, done: false}
console.log(fn.next()); // {value: 2, done: false}
console.log(fn.next()); // {value: 3, done: true}

当调用 next 方法时,返回一个对象,它的 value 属性就是当前 yield 表达式的值,done 属性的值表示遍历是否结束。

上文的 genFn 方法中,我们只执行了两个异步操作,万一异步操作多起来,又会陷入回调地狱了,我们把这里的逻辑封装一下:

function step(nextFn) {
  const next = (err, data) => {
    let res = nextFn.next(data);
    // 如果 res.done 为 true,才表示迭代结束,返回
    if (res.done) return;
    // 否则执行递归的逻辑
    res.value(next);
  };
  next();
}
step(genFn());

这里有一个递归的过程,我们把这一步封装称为自动执行 Generator 函数。

而 async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

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

// 等同于

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

我们介绍了这么多,终于回到了本题的 myAsync 函数,本题我们要实现 async 的功能,就需要返回一个Promise 实例,把 Generator 函数中状态为 done 的值 resolve 出来,把错误的信息 reject 出来,最终代码实现如下:

function myAsync(genFn) {
  // 返回一个 Promise 实例
  return new Promise(function (resolve, reject) {
    const gen = genFn();
    // 自动执行器的封装,里面是递归的逻辑
    function step(nextFn) {
      let next;
      //
      try {
        next = nextFn();
      } catch (e) {
        return reject(e);
      }
      // 如果已经到 done 状态了,就 resolve 最终的值
      if (next.done) {
        return resolve(next.value);
      }
      // 不是 done 状态,说明程序还没执行完,就继续递归
      Promise.resolve(next.value).then(
        function (v) {
          step(function () {
            return gen.next(v);
          });
        },
        function (e) {
          // 错误的逻辑 reject 出来
          step(function () {
            return gen.throw(e);
          });
        }
      );
    }
    step(function () {
      return gen.next();
    });
  });
}

这样我们就实现了 myAsync 函数,但实现这个函数并不是重点,重点是学习 Generator 函数的用法以及理解 async 是如何通过 Generator 函数来实现的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

懒羊羊h

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值