利用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 手写题集锦
相关推荐
y先森5 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy5 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189115 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿6 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡7 小时前
commitlint校验git提交信息
前端
虾球xz7 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇8 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒8 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员8 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐8 小时前
前端图像处理(一)
前端