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
来将含有较新语法的代码向下编译为兼容运行环境的代码。