Node.js中异步操作的代码演进

node.js 4.x版本增加了许多ES6语法特性(如const/let/class/箭头函数)的支持

node.js 6.x版本囊括了绝大多数的ES6语法特性以及部分ES7特性

node.js 8.x版本更支持了ES8语法(如async/await

此后的版本也在频繁不断地更新,纳入许多新特性。

关于NodeJS中异步函数的写法,也在不断进行改善优化:

1. 嵌套回调函数

const fs = require('fs')

fs.readFile('demo.json', (err, data) => {
  if(err) return console.log(err)
  data = JSON.parse(data)
  console.log(data.name)
})

这是NodeJS中比较原始的一种写法,将回调函数作为异步函数的参数,当异步操作过多,函数不断嵌套,很容易形成回调地狱。

2. Promise

function readFileAsync(path){
  return new Promise((resolve, reject) => {
    fs.readFile(path, (err, data) => {
      if(err) reject(err)
      else resolve(data)
    })
  })
}

readFileAsync('demo.json')
  .then(data => {
    data = JSON.parse(data)
    console.log(data.name)
  })
  .catch(err => {
    console.log(err)
  })

NodeJS中开始原生支持Promise,我们可以将异步函数封装成Promise,方便后续异步操作,摆脱了不断嵌套的回调函数。

3. Promisify

const util = require('util')

util.promisify(fs.readFile)('demo.json')
  .then(JSON.parse)
  .then(data => {
    console.log(data.name)
  })
  .catch(err => {
    console.log(err)
  })

NodeJS8.x版本在util模块中新增了一个工具函数promisify,它将一个接收回调函数参数的函数转换成一个返回Promise的函数。这样一来我们可以省略自己封装Promise函数的过程,大大减少了代码体积。

4. Generator

Promise的出现解决了回调函数地狱问题,将函数嵌套改成了链式调用,但是紧接着又迎来了新的问题:Promise使用了then方法来加载执行回调函数,当业务比较复杂的时候,一连串的then让代码显得比较冗余,并且语义也不清楚。

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

ES6中的generator函数就是一个初步的解决方案,和Promise一起让异步代码能写得更加清晰明确。

generator函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。它可以交出函数的执行权,并与函数体内外进行数据交换。

var fetch = require('node-fetch')

// ----generator函数----
function* gen(){
  var url = 'https://api.github.com/users/github'
  var result = yield fetch(url)
  console.log(result.bio)
}

// ----执行器----
//返回迭代器对象
var g = gen()
//执行next获取结果对象
var result = g.next()
//response对象是一个Promise
result.value.then(function(data){
  //data.json()也是一个Promise
  return data.json()
}).then(function(data){
  // 往next()传入参数,会进入函数体,作为上阶段异步任务的返回结果(变量result)
  g.next(data)
})

从以上代码可以看出,虽然generator函数将异步操作表示得很清晰,但是需要编写执行器来进行流程管理,使其自动运行。

著名程序员TJ Holowaychuk发布了co模块来帮助执行generator函数,模块内部针对yield命令后的各种数据类型分别编写了自动执行器,此时我们定义完generator函数后,只需要co(gen)即可自动执行。

const co = require('co')

var gen = function* (){
  var f1 = yield readFile('demo1.json')
  var f2 = yield readFile('demo2.json')
  console.log(f1.toString())
  console.log(f2.toString())
}

co(gen)

此后出现的async函数其实就是generator函数的语法糖,语义更加明确,并且内置执行器,不需要co模块或手动调用next方法,所以当async/await出现后,被称为是异步操作的终极解决方案,generator函数自然就被取代了。

co.js也是著名Node.js框架Koa1的核心依赖库,而当async/awaitNode.js中原生支持后,co.js也停止了维护,依赖于async/awaitKoa2开始普及。

关于generator详细请参考Generator 函数的异步应用

5. async/await

随着ES8规范中明确了async/await的语法,NodeJS8.x版本也加入了相应特性的支持,我们可以使用这个异步操作的“终极解决方案”来让代码更加简洁、清晰、易读。

const fs = require('fs')
const util = require('util')
const readAsync = util.promisify(fs.readFile)

async function read(){
  try {
    let data = await readAsync('demo.json')
    data = JSON.parse(data)
    console.log(data.name)
  } catch (err) {
    console.log(err)
  }
}

read()

如果服务器上的NodeJS版本较低,并不支持这些新的语法特性,那么我们可以使用Babel来将含有较新语法的代码向下编译为兼容运行环境的代码。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章