侧边栏

理解Generator函数与async函数

发布于 | 分类于 编程语言/JavaScript

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

参考

JavaScript中的异步编程

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

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

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

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

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

Generator函数

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

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

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

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

html
<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 方法交回执行权

thunk函数

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

js
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);

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

js
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函数的步骤为

js
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)传入数据,因此可以通过递归自动实现

js
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函数

使用Promise

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

先将readFile修改成Promise形式

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

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

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

手动调用方式为

js
var g = gen();

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

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

js
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对象

async 和 await

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

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

基本方式

async函数本身返回一个promise

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

const result = testAsync();

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

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

js
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

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

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

js
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

错误处理

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

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

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

小结

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

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。