2

都说async/awaitGenerator+Promise的语法糖,通过本文逐步揭开async/await背后的秘密...

一、使用Generator把异步逻辑同步化

function asyncOp(x) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            Number.isSafeInteger(x) 
            ? resolve(12)
            : reject(new Error('Invalid integer'));
        }, 3000)
    })
}

function *gen(x) {
    try {
        var y = yield asyncOp(x);    
        return x + y;
    } catch(e) {
        console.error(e)
    }
}

var g = gen(1);
// 获取异步操作
var asyncAction = g.next();

asyncAction.value
.then(value => {
    // 把异步操作的结果值传给生成器函数
    var result = g.next(value);
    console.log(result.value);
})

整体思路:

  1. 通过yield返回异步操作(并暂停生成器函数执行);
  2. 通过next方法把异步操作的结果值传入生成器函数(并继续执行生成器函数);
    相对于生成器函数里代码来说并不关心yield表达式的值是同步还是异步。
  3. 如果异步操作失败了,可以跟通过throw方法在暂定位置抛异常。
var g = gen('a');
var asyncAction = g.next();

asyncAction.value
.catch(reason => {
    // 通过throw告诉生成器函数异常操作发生了异常
    g.throw(reason);
})

二、实践:异步加法

function getRadom() {
  return (Math.random() * 100) >>> 0;
}

function getRandomAsync() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(getRadom());
    }, 2000)
  })
}

function* sum() {
  var x = yield getRandomAsync();
  console.log(`x=${x}`)
  var y = yield getRandomAsync();
  console.log(`y=${y}`)
  return x + y;
}

1. 最搓的方式:逐步调用

var gen = sum();
// 获取异步操作1
gen.next().value
.then(val => {
  // 把异步操作1的结果传给生成器函数,并获取异步操作2
  gen.next(val).value
  .then(val => {
    // 把异步操作2的结果传给生成器函数,并获最终结果
    var sum = gen.next(val).value;
    console.log(`sum=${sum}`)
  })
})

上面的写法就像记流水账,如果有3个数相加还这样写岂不是要疯。

2. 固定的模式的调用方式:

// 生成器函数执行器
function runner(genFunc) {
  // 创建生成器对象
  var gen = genFunc();
  // 开启执行
  return doRun();

  function doRun(arg) {
    // 把上一个异步操作结果`arg`传如生成器函数
    var data = gen.next(arg);
    
    if(data.done) {
      return Promise.resolve(data.value);
    }
    // 如果还没结束,就等异步操作结束后递归调用doRun。
    return Promise.resolve(data.value).then(doRun);
  }
}

runner(sum).then(sum => {
  console.log(sum)
})

注意:
这里使用Promise.resolve方法data.value转成Promise,因为Promise.resolve的特殊功能:如果实参value是个Promise对象,则直接返回实参

三、实践:处理异步操作的异常

function runner(genFunc) {
  var gen = genFunc(); 

  return new Promise((resolve, reject) => {
    doRun();
    
    function doRun(arg) {
      try {
        // 捕获`next`方法抛出的异常
        var data = gen.next(arg);
        if(data.done) {
          return resolve(data.value);
        }

         // 如果还没结束,就等异步操作结束后递归调用doRun。
        return Promise.resolve(data.value)
            .then(doRun)
            .catch(gen.throw) // 通过`throw`方法告诉生成器异常了        
            .catch(reject) // 捕获`throw`方法抛出的异常(即生成器方法没有捕获处理异常)        
      } catch(error) {
        reject(error);
      }   
    }                  
  })   
}

runner(sum).then(sum => {
  console.log(sum)
})
.catch(reason => {
  console.error(reason)
})
  1. 异常不仅来自throw方法,next方法也可能会抛出异常,所以在最外层使用try-catch捕获next方法抛出的异常;
  2. runner方法的返回值也不再是doRun()了,而改成了Promise,用于处理next方法抛出的异常和最终的结果值。

捕获throw方法抛出的异常也可以采用try-catch方式,这样就跟捕获next方法抛出的异常保持一致了:

function runner(genFunc) {
    var gen = genFunc();
 
    return doRun();

    function doRun(arg) {
        return new Promise((resolve, reject) => {
            step('next');

          function step(methodName, arg) {
            try {
                // 捕获`next`方法抛出的异常
                var data = gen[methodName](arg);
            } catch(error) {
                reject(error);
                return;
            }

            if(data.done) {
                return resolve(data.value);
            }

            return Promise.resolve(data.value)
                .then(value => {
                    step('next', value);
                })
                .catch(reason => {
                    step('throw', reason);
                })
          }
                    
        })   
    }
}

runner(sum)
.then(sum => {
    console.log(sum)
})
.catch(reason => {
    console.error(reason)
})

四、实践:分析async/awaitGenerator+Promise写法

Babel如何把async转成Generator?

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg){
    try{
        var info = gen[key](arg);
        var value = info.value;
    }catch(error){
        reject(error);
        return;
    }
    
    if(info.done){
        resolve(value);
    }else{
        Promise.resolve(value).then(_next, _throw);
    }
}

// 负责把`async`转成`generator`
function _asyncToGenerator(fn) {
    return function () {
        // 处理传给生成器的参数
        var self = this,
            args = arguments;
            
        return new Promise(function (resolve, reject){
             // 生成器的函数在Promise参数的回调函数里执行,并且处理参数
            var gen = fn.apply(self, args);

            function _next(value){
                asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
            }

            function _throw(err){
                asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
            }
            _next(undefined);
        });
    };
}

使用固定的模式把async转成Generator,Babel的实现更严谨些:

  1. 可以传参数给生成器函数;
  2. 先转Generator,调用时再传参。

参考这个调整之前的实现:

function _asyncToGenerator(genFunc) {
    return function() {
        var args = arguments;
        var self = this;

        return new Promise((resolve, reject) => {
              var gen = genFunc.apply(self, args); 
              step('next');

          function step(methodName, arg) {
                try {
                    var data = gen[methodName](arg);
                } catch(error) {
                      reject(error);
                      return;
                }

                if(data.done) {
                    return resolve(data.value);
                }

                return Promise.resolve(data.value)
                    .then(value => {
                        step('next', value);
                    })
                    .catch(reason => {
                        step('throw', reason);
                    })
            }            
        })  
    }
}

_asyncToGenerator(sum)()
.then(sum => {
    console.log(sum)
})
.catch(reason => {
    console.error(reason)
})

总结下async/await转成Genertor方式:

  1. await直接替换成yield;
  2. async函数体代码转成生成器代码(匿名的生成器函数);
  3. async函数名被转成普通函数内部调用生成器函数的函数。
async function sum() {
    var x = await 1;
      var y = await 2;
      return x + y;
}

// 对应的生成器方式
function sum() {
    return _sum.apply(this, arguments);
}

function _sum() {
  // 注意:函数体内的_sum变量是个局部变量,不影响外部作用域下的_sum的值。
  _sum = _asyncToGenerator(function* () {
    var x = yield 1;
    var y = yield 2;
    return x + y;
  });

  // 创建生成器对象,并开始执行生成器
  return _sum.apply(this, arguments);
}

参考

  1. 深入Generator——异步

普拉斯强
2.7k 声望53 粉丝

Coder