理解Generator函数与async函数

最近看见了一个async函数在event loop中的执行顺序问题,突然发现我对于Generator函数与async的掌握十分有限,惊出一身冷汗,赶忙恶补一番。

<!--more-->

参考

1. JavaScript中的异步编程

JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数

回调函数本身并没有问题,它的问题出现在:如果需要连续处理多个异步任务,就会编程多个回调函数嵌套,形成著名的“回调地狱”

Promise就是为了解决回调地狱而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,修改为通过promise.then的链式调用。

Promise 的最大问题是代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。

2. Generator函数

Generator生成器函数在执行时能暂停,后面又能从暂停处继续执行。

调用一个生成器函数并不会马上执行它里面的语句,,而是返回一个这个生成器的 迭代器**(iterator )对象(可以理解为一个指针)。调用指针 g 的 next 方法,会移动内部指针(即执行异步任务的第一段(后续)),指向第一个(后续)遇到的 yield 语句。

next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。

下面是一个使用Generator函数切换按钮状态的例子,注意这个函数可以一直执行下去,但会while(true)并不会造成死循环,因为每次循环都需要调用g.next()触发

<button id="btn">false</button>
<script type="text/javascript">
    function *toggleBtnStatus(){
        let status = false;
        while(true) {
            status = !status;
            btn.innerText = status;
            yield status;
        }
    }
    let g = toggleBtnStatus();
    btn.onclick = function(){
        g.next()
    }
</script>

Generator函数的一个问题是需要手动一直调用next,直至状态done为true为止, Generator 函数自动执行有两种方案

  • 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
  • Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权

2.1. thunk函数

在JavaScript中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数

var Thunk = function(fn){
  return function (){
    var args = Array.prototype.slice.call(arguments);
    return function (callback){
      args.push(callback);
      return fn.apply(this, args);
    }
  };
};

var readFile = Thunk(fs.readFile);

// 转换后的函数只接收callback作为参数
readFile(fileA)(callback);

下面是读取多个文件的处理方案

var gen = function* (){
  var r1 = yield readFile('/etc/fstab');
  console.log(r1.toString());
  var r2 = yield readFile('/etc/shells');
  console.log(r2.toString());
};

根据MDN文档描述,调用 next()方法时,如果传入了参数,那么这个参数会作为上一条执行的 yield 语句的返回值,那么手动情况下,执行上面gen函数的步骤为

var g = gen();
var r1 = g.next();
r1.value(function(err, data){
  if (err) throw err;
  var r2 = g.next(data);
  r2.value(function(err, data){
    if (err) throw err;
    g.next(data);
  });
});

可见上面都是通过g.next(data)传入数据,因此可以通过递归自动实现

function run(fn) {
  var gen = fn();

  // 这个next函数实际上就是上面readFile 的thunk函数的callback参数
  function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);
  }

  next();
}

run(gen);

这样就实现了Generator函数的自动执行,需要保证每个yeild后面的返回值是thunk函数

2.2. 使用Promise

参考co库的实现,使用promise与thunk的方式类似,通过在promise.then中调用gen.next来自动执行函数

先将readFile修改成Promise形式

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

然后,对于同样的异步读取文件的任务

var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

手动调用方式为

var g = gen();

g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(data);
  });
})

同thunk函数,这里可以改写为

function run(gen){
  var g = gen();

  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }

  next();
}

run(gen);

同样也达到了自动执行gen函数的目的,这里需要保证每个yeild后面的返回值是一个Promise对象

3. async 和 await

上面整理了Generator 函数的使用和自动执行Generator 函数的实现原理。

由于自动执行生成器函数需要额外进行处理,因此在ES7引入了async和await,使用一对关键字asyncawait来实现异步代码的同步书写方式。实际上,async 函数就是 Generator 函数的语法糖,包括内置执行器、await后可跟任意数据类型等方面。

3.1. 基本方式

async函数本身返回一个promise

async function testAsync(){
    return "hello";
}

const result = testAsync();

// console.log(result); // Promise { hello }
result.then(res=>{
    console.log(res)
})

await后跟一个表达式。await可以阻塞当前async函数中的代码,并等待其后面的表达式执行完成,然后取消阻塞并继续执行后续代码。

function timer(){
    return new Promise((res, rej)=>{
        setTimeout(()=>{
            res("hello");
        }, 500)
    })
}

async function getTimer(){
    let data = await timer(); // 阻塞 500ms
    console.log(data);
}

getTimer();

理解await十分重要,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。

  • 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
  • 如果它等到的是一个 Promise 对象,那么它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果

想想一个Promise对象是如何resolve的:异步等待Promise的状态修改为FulFilled时调用resolve

new Promise((resolve, reject)=>{
    setTimeout(() => {
        resolve(123)
    }, 0);
})

换句话说,async 函数调用本身不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行(awiati后面的代码会被阻塞)。

async function t(){
    console.log('async start')
    return 'async result'
}
t().then(res=>{
    console.log(res)
})
console.log('script start')

上面代码依次输出 async start、script start、async result

3.2. 错误处理

由于await后面的promise可能被reject,所以最好将await代码放在try-catch语句中。 不过也可以直接在promise上使用catch完成异常捕获

async function getTimer(){
    let data = await timer().catch(e=>{
        console.log(e);
    });
    console.log(data);
}

从这里可以看出,只需要把异步的操作放在await后面的表达式即可,因此Promise的一些特性比如Promise.all也就可以使用了

4. 小结

这里整理了generator函数的基本使用和自动执行方法,然后介绍了作为自带执行器的generator函数语法糖——async函数。理解这些基础的语法是非常有必要的。