利用Generator函数实现async、await

Generator函数的简单介绍


从形式上来看,Generator函数和普通函数别无二致。只有两个区别,一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态。

js 复制代码
function* helloWorldGenerator() {
    yield 'hello';
    yield 'world';
    return 'ending';
  }
 const hw = helloWorldGenerator();

上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(helloworld),即该函数有三个状态:hello,world 和 return 语句(结束执行)。

Generator函数执行之后会返回一个遍历器对象,这个遍历器对象主要有next、throw、return这三个方法,实现async和await主要用到next、throw这两个方法。

next方法

执行完Generator函数返回的遍历器对象可以看作是一个指针对象,每次调用next方法,就会使得指针对象指向下一个状态,函数就会从上一次停下的状态继续往后执行,直到遇到下一个yield或者return,next方法返回一个对象,这个对象有value、done这两个属性。

js 复制代码
  console.log(hw.next());   // { value: 'hello', done: false }
  console.log(hw.next());   // { value: 'world', done: false }
  console.log(hw.next());   // { value: 'ending', done: true }
  console.log(hw.next());   // { value: undefined, done: true }

value属性的值就是yield表达式的值,当函数遇到return,返回的对象的value属性就是return的返回值,done属性就会变成true,因为return就标志着这个函数完成了全部的执行;当一个函数内部没有return的时候,相当于return undefined,所以在遍历完最后一个yield状态之后还要执行一遍next方法,done属性才变为true。

next方法的参数

next方法可以携带一个参数,这个参数会作为上一个yield表达式的返回值(注意yield表达式的值和返回值的区别)。

js 复制代码
function* foo(x) {
    const y = 2 * (yield (x + 1));
    const z = yield (y / 3);
    return (x + y + z);
}
const b = foo(5);
console.log(b.next());// { value:6, done:false }
console.log(b.next(12)); // { value:8, done:false }
console.log(b.next(13)); // { value:42, done:true }

上面代码中,在调用第2个和第3个next方法的时候分别传入12和13作为参数,则函数中的y的值就为2 * 12,z的值就为13,最后函数返回的值就为5 + 24 + 13

throw方法

通过调用throw方法,可以在Generator函数外部抛出错误,然后在函数内部被捕获。

js 复制代码
const g = function* () {
    try {
      yield;
    } catch (e) {
      console.log(e);
    }
  };
const i = g();
i.next();
i.throw(new Error('出错了!'));  // Error: 出错了!(...)

具体实现


基础版本

了解了Generator函数的上述特性之后,我们就可以来着手实现async/await了。

js 复制代码
function fn(nums) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(nums * 2);
        }, 1000);
    });
}
function* gen() {
    const num1 = yield fn(1);
    console.log(num1); // 2
    const num2 = yield fn(num1);
    console.log(num2); // 4
    const num3 = yield fn(num2);
    console.log(num3); // 8
    return num3;
}
const testGAsync = asyncToGenerator(gen);

实现async/await核心主要是要实现在函数的内部将异步操作转为继发操作,即要等到上一个异步操作完成并返回值,再进行下一步操作,很显然我们的Generator函数是可以实现的,我们将我们的每一个异步操作用Promise包装,只有当当前的promise对象状态变为fulfilled,我们才继续对Generator实例执行next()方法,同时将上一步中promise对象的返回值作为next参数传入Generator函数。

js 复制代码
function asyncToGenerator(generatorFunc) {
  return function(...args) {
    // 执行Generator函数,返回遍历器对象。
    const gen = generatorFunc.apply(this,args);  
    function step(arg) {
      // 对遍历器对象执行next方法
      const generatorResult = gen.next(arg);
      const { value , done } = generatorResult;
      if(done) {
        resolve(value);
      } else {
        Promise.resolve(value).then(val => step(val));
      }
    }
    step();
  }
}

上面的代码初步实现了async/await,核心思路就是利用递归,不断地去执行遍历器对象的next方法,执行next方法返回的是一个Promise对象,我们在Promise的then回调函数中将我们异步操作得到的值传递给下一个next方法作为参数传入Generator函数中,成为yield表达式的返回值。

错误捕获

我们的基础版本和真正的async/await还有两个区别,第一,我们无法捕获我们异步操作过程中抛出的错误;第二,async/await返回的是一个Promise对象,其可以对async函数内部的同步代码的错误进行捕获。

js 复制代码
// 1. 无法在async函数中捕获异步操作的错误
function fn(nums) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('promiseError');
        }, 1000);
    });
}
function* gen() {
    try {
        const num1 = yield fn(1);
        console.log(num1); 
    } catch(err) {
        console.log(err);
    }
}
const testGAsync = asyncToGenerator(gen);
testGAsync();  // Uncaught (in promise) promiseError

// 2. 无法在返回的Promise对象中捕获同步错误
function* gen() {
    const num1 = yield fn(1);
    console.log(num1); 
    throw new Error('genError')
}
const testGAsync = asyncToGenerator(gen);
testGAsync().then(res => {  
    console.log(res);
}, err => {
    console.log(err)
});  // Uncaught (in promise) Error: genError

为了能够捕获到同步错误和异步错误,我们需要做两个操作,一个操作是去尝试捕获在调用next方法时候可能有的错误错误,捕获到的话就在Promise对象内部reject出去,第二个操作是用Promise对象对next方法返回值进行再次包装时添加then方法的第二个回调函数,进行错误捕获,捕获到错误就调用遍历器对象的throw方法让错误在async函数内部被捕获。

js 复制代码
function asyncToGenerator(generatorFunc) {
    return function () {
        let gen = generatorFunc.apply(this, arguments)
        return new Promise((resolve, reject) => {
            function step(name, arg) {
                let generatorResult;
                try {
                    generatorResult = gen[name](arg)
                } catch (err) {
                    reject(err)
                }
                const { value, done } = generatorResult
                if (done) {
                    resolve(value)
                } else {
                    Promise.resolve(value).then(
                        (val) => {
                            step('next', val)
                        },
                        (err) => {
                            step('throw', err)
                        }
                    );
                }
            }
            step('next')
        })
    }
}

参考:

  1. 阮一峰 ES6标准入门教程
  2. JavaScript 手写题集锦
相关推荐
anyup_前端梦工厂4 分钟前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand8 分钟前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL25 分钟前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿26 分钟前
react防止页面崩溃
前端·react.js·前端框架
z千鑫1 小时前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js
m0_748256141 小时前
前端 MYTED单篇TED词汇学习功能优化
前端·学习
小白学前端6662 小时前
React Router 深入指南:从入门到进阶
前端·react.js·react
web130933203983 小时前
前端下载后端文件流,文件可以下载,但是打不开,显示“文件已损坏”的问题分析与解决方案
前端
outstanding木槿3 小时前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架
好名字08214 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架