[关闭]
@brizer 2016-02-09T13:14:46.000000Z 字数 3604 阅读 1144

异步操作


前言

本文学习了javascript中的异步操作方式。


基本概念

javascript是单线程运行的,所以异步显得格外重要。
ES6诞生以前,异步编程的方法,大概有下面四种。

回调函数
事件监听
发布/订阅
Promise 对象

ES6将JavaScript异步编程带入了一个全新的阶段,ES7的Async函数更是提出了异步编程的终极解决方案。

异步

异步,简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。

回调函数

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

  1. fs.readFile('/etc/passwd', function (err, data) {
  2. if (err) throw err;
  3. console.log(data);
  4. });

readFile函数的第二个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了/etc/passwd这个文件以后,回调函数才会执行。

Promise

回调函数本身并没有问题,它的问题出现在多个回调函数嵌套,这样就会造成回调地狱。

Promise就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的横向加载,改成纵向加载。采用Promise,连续读取多个文件,写法如下:

  1. var readFile = require('fs-readfile-promise');
  2. readFile(fileA)
  3. .then(function(data){
  4. console.log(data.toString());
  5. })
  6. .then(function(){
  7. return readFile(fileB);
  8. })
  9. .then(function(data){
  10. console.log(data.toString());
  11. })
  12. .catch(function(err) {
  13. console.log(err);
  14. });

Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。

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


generator

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

整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。Generator函数的执行方法如下。

  1. function* gen(x){
  2. var y = yield x + 2;
  3. return y;
  4. }
  5. var g = gen(1);
  6. g.next() // { value: 3, done: false }
  7. g.next() // { value: undefined, done: true }

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

看看如何使用generator来完成异步操作:

  1. var fetch = require('node-fetch');
  2. //封装一个异步擦做,先从接口获取数据然后从json格式中解析数据
  3. function* gen(){
  4. var url = 'https://api.github.com/users/github';
  5. var result = yield fetch(url);
  6. console.log(result.bio);
  7. }
  8. //获取遍历器对象
  9. var g = gen();
  10. var result = g.next();
  11. //由于Fetch模块返回的是一个Promise对象,因此要用then方法调用下一个next 方法
  12. result.value.then(function(data){
  13. return data.json();
  14. }).then(function(data){
  15. g.next(data);
  16. });

async

ES7提出了async函数,generator的一个语法糖。

  1. var fs = require('fs');
  2. var readFile = function (fileName){
  3. return new Promise(function (resolve, reject){
  4. fs.readFile(fileName, function(error, data){
  5. if (error) reject(error);
  6. resolve(data);
  7. });
  8. });
  9. };
  10. var gen = function* (){
  11. var f1 = yield readFile('/etc/fstab');
  12. var f2 = yield readFile('/etc/shells');
  13. console.log(f1.toString());
  14. console.log(f2.toString());
  15. };

上面的代码改成async来实现:

  1. var asyncReadFile = async function (){
  2. var f1 = await readFile('/etc/fstab');
  3. var f2 = await readFile('/etc/shells');
  4. console.log(f1.toString());
  5. console.log(f2.toString());
  6. };

一比较就会发现,async函数就是将Generator函数的星号(*)替换成async,将yield替换成await,仅此而已。

同Generator函数一样,async函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。


总结

我们通过一个例子,来看Async函数与Promise、Generator函数的区别。

假定某个DOM元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。

首先是Promise的写法。

  1. function chainAnimationsPromise(elem, animations) {
  2. // 变量ret用来保存上一个动画的返回值
  3. var ret = null;
  4. // 新建一个空的Promise
  5. var p = Promise.resolve();
  6. // 使用then方法,添加所有动画
  7. for(var anim in animations) {
  8. p = p.then(function(val) {
  9. ret = val;
  10. return anim(elem);
  11. })
  12. }
  13. // 返回一个部署了错误捕捉机制的Promise
  14. return p.catch(function(e) {
  15. /* 忽略错误,继续执行 */
  16. }).then(function() {
  17. return ret;
  18. });
  19. }

虽然Promise的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是Promise的API(then、catch等等),操作本身的语义反而不容易看出来。

接着是Generator函数的写法。

  1. function chainAnimationsGenerator(elem, animations) {
  2. return spawn(function*() {
  3. var ret = null;
  4. try {
  5. for(var anim of animations) {
  6. ret = yield anim(elem);
  7. }
  8. } catch(e) {
  9. /* 忽略错误,继续执行 */
  10. }
  11. return ret;
  12. });
  13. }

上面代码使用Generator函数遍历了每个动画,语义比Promise写法更清晰,用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于,必须有一个任务运行器,自动执行Generator函数,上面代码的spawn函数就是自动执行器,它返回一个Promise对象,而且必须保证yield语句后面的表达式,必须返回一个Promise。

最后是Async函数的写法。

  1. async function chainAnimationsAsync(elem, animations) {
  2. var ret = null;
  3. try {
  4. for(var anim of animations) {
  5. ret = await anim(elem);
  6. }
  7. } catch(e) {
  8. /* 忽略错误,继续执行 */
  9. }
  10. return ret;
  11. }

可以看到Async函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将Generator写法中的自动执行器,改在语言层面提供,不暴露给用户,因此代码量最少。如果使用Generator写法,自动执行器需要用户自己提供。


参考

异步操作和async函数

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注