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來將含有較新語法的代碼向下編譯爲兼容運行環境的代碼。

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