JavaScript异步编程

异步变成对于js来说非常重要,解决异步应用的方法也在不断更新,有人说终极目标就是使用同步的语法解决异步问题。下面我们就来看下异步编程的几种解决方案。

同步和异步

同步

异步

Callback

回调函数:把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。

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

回调函数的缺点是在多个回调函数嵌套的时候会横向发展,出现“回调地狱”(回调函数噩梦/callback hell),阅读起来也比较恼火

fs.readFile(fileA, function (err, data) {
  fs.readFile(fileB, function (err, data) {
    // ...
  });
});

Promise

Promise解决回调的方式其实是纵向发展,虽然解决了回调地狱,但是一大堆then,语义不清楚。

var readFile = require('fs-readfile-promise');

readFile(fileA)
.then(function(data){
  console.log(data.toString());
})
.then(function(){
  return readFile(fileB);
})
.then(function(data){
  console.log(data.toString());
})
.catch(function(err) {
  console.log(err);
});

Generator/yield

协程

多个线程互相协作,完成异步任务:

  • 第一步,协程A开始执行。
  • 第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
  • 第三步,(一段时间后)协程B交还执行权。
  • 第四步,协程A恢复执行。
    Generator最大的特点就是可以交出函数的执行权(即可以暂停)。
function* gen(x){
  var y = yield x + 2;
  return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }

调用 Generator 函数,会返回一个内部指针(即遍历器 )g 。这是 Generator 函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。

调用指针 g 的 next 方法,会移动内部指针(即执行异步任务的第一段),每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。

  • value 属性是 yield 语句后面表达式的值,表示当前阶段的值
  • done 属性是一个布尔值,表示 Generator函数是否执行完毕,即是否还有下一个阶段。

Generator调用的时候不会自动执行(不自带执行器),必须通过执行next()方法来将内部指针移动到下一步。

错误处理

使用try...catch在Generator 函数内部部署错误处理代码,用于捕获函数体外抛出的错误。

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

var g = gen(1);
g.next();
g.throw('出错了');
// 出错了

使用方法

var fetch = require('node-fetch');
//Fetch 模块会返回一个 Promise 对象
function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}
//调用
var g = gen();
var result = g.next();

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

其他

由于Generator不自带执行器,所以出现了一些工具来自动执行Generator的next(),具体的可以参见:

Async/await

async函数是基于Generator函数实现的,也就是说是Generator函数的语法糖。相比较与Generator,有如下特点:

  • 内置执行器:可直接调用,不用手动或者依赖其他函数执行
  • 语义更强:async关键词相比*、await相比yield都更加让人容易理解
  • 返回Promise:返回值为Promise,可使用then(包括catch)方法执行下一步操作,更加方便
async function getStockPriceByName(name) {
 var symbol = await getStockSymbol(name);
 var stockPrice = await getStockPrice(symbol);
 return stockPrice;
}

getStockPriceByName('goog').then(function (result){
 console.log(result);
});
  • 更广的适用性: co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)
  • ES7:async为ES7版本引入。

异步编程的最高境界,就是根本不用关心它是不是异步,很多人认为Async是终极解决方案。

我们可以和Generator对比下:

// Generator
var fs = require('fs');

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());
};
gen.next()
gen.next()
gen.next()

//async
var asyncReadFile = async function (){
  var f1 = await readFile('/etc/fstab');
  var f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

错误处理

await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中或者使用catch接收错误。

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}

// 另一种写法

async function myFunction() {
  await somethingThatReturnsAPromise().catch(function (err){
    console.log(err);
  });
}

愿你走出半生,归来仍是少年