Node.js中的异步调用演变

Node.js里的异步调用是通过回调函数实现的,不管javascript的语言特性如何转变,至少在Node.js的native api里,肯定是回调函数。

比如读取文件

fs.readFile('/home/wqf/a.txt', function (err, data) {
  if (err) throw err;
  console.log(data);
  fs.readFile('/home/wqf/b.txt', function (err, data) {
    if (err) throw err;
    console.log(data);
  });
});

回调函数面临一个最大的问题就是callback hell,也就是多个异步操作有先后顺序时,会嵌套很多的回调函数。


为了防止这种情况,后来es6添加了yield关键字,我们看看它是如何运行的

function* asnycProc() {
  var data1 = yield readFile('/home/wqf/a.txt');
  console.log(data1);
  var data2 = yield readFile('/home/wqf/b.txt');
  console.log(data2);
}

 这个方法要做的事情和上面的回调写法没啥区别,实际上可以把每个yield语句以后的处理,当成yield语句所调用方法的回调。

但是毕竟后面的处理并不是一个方法,所以也不能用普通的方式进行回调,那么后面的处理是怎么执行的呢?

答案是next()

var g = asnycProc();
g.next();
g.next();

但是我们知道native api是通过回调方法执行的,默认不可能执行next()方法,那么就必须包装一下,通过包装一个执行next()的回调方法给native api用,但是next()方法是通过上面的形式执行的,我们没有办法直接把next()包装到回调方法里,必须得借助外力来执行这个next()。

这里有两种方法,一种是通过thunk函数,一种是通过promise对象。

关于thunk函数的介绍可以看这篇

http://www.ruanyifeng.com/blog/2015/05/thunk.html

目的就是把方法的其他参数跟回调方法参数分离,最终生成的方法就只有一个回调方法参数。

写个简单的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);
    }
  };
};

 那么上面的readFile方法就可以写成

var readFile = Thunk(fs.readFile);
readFile('/home/wqf/a.txt');这个方法返回了一个只能接收回调函数的方法

 那么我们要做的事情就是下面这个语句返回的是native api里回调函数的参数data

yield readFile('/home/wqf/a.txt');

这又是怎么做到的呢?对于Generator方法,第一个yield语句的返回值是通过第二个next()方法传参回去的。

那么可以通过执行一下方法实现

var g = asnycProc();

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

这里r1.value,就是yield readFile('/home/wqf/a.txt');语句中yield后面的值,也就是一个只可以接受回调方法参数的方法。然后再通过第二个g.next(data)方法把结果传了回去。

上面的这个执行函数顺序执行的处理是一样的,所以可以用递归来执行。

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

  function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);
  }

  next();
}

run(asnycProc);

接下来再看看promise的实现方法

把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 g = asnycProc();

g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(data);
  });
})
 同样这个方法也可以用递归来实现
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(asnycProc);
  既然不管用哪种方法,都需要一个执行器,为什么不能把这个处理弄成语言的语法糖呢,es7的async/await便是做这个的,写法和generator一样,只是把*换成了async,把yield换成了await。
var asyncReadFile = async function (){
  var f1 = await readFile('/etc/fstab');
  var f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
 调用很简单只要直接写
asyncReadFile();
便会自动用执行器执行,await后面跟的必须是一个promise对象,所以其实本质上还是使用的第二种方法。

猜你喜欢

转载自weiqingfei.iteye.com/blog/2259499