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)
})
NodeJS
8.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/await
在Node.js
中原生支持後,co.js
也停止了維護,依賴於async/await
的Koa2
開始普及。
關於generator
詳細請參考Generator 函數的異步應用。
5. async/await
隨着ES8規範中明確了async/await
的語法,NodeJS
8.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
來將含有較新語法的代碼向下編譯爲兼容運行環境的代碼。