避免Zalgo
Zalgo: 一种 JavaScript 开发人员虚构的疯狂恶魔,取名 Zalgo,用来描述 JavaScript 中同步 / 异步的混乱。Zalgo是一个互联网传说,会导致世界错乱,死亡和毁灭的一个不详实体。
有兴趣的同学可以在如下网址上找到Isaac Z. Schlueter的原始帖子
https://blog.izs.me/2013/08/designing-apis-for-asynchrony
不可预测的危险的函数
我们来看一下下面这个危险的函数,它希望实现的目的是如果没有缓冲是异步读取文件,如果有缓存则同步读取缓存。请大家注意,这并不是一个好的实践。它的行为是难以预测的。
const fs = require('fs')
const cache = {}
function inconsistentRead(filename, cb) {
if (cache[filename]) {
// 同步调用!!!
cb(cache[filename])
} else {
// 异步调用!!!
fs.readFile(filename, 'utf8', (err,data) => {
cache[filename] = data
cb(data)
})
}
}
解决上述的问题
我们需要明确,API明确定义其特性:同步的或者是异步的
使用同步API
使用fs.readFileSync()函数替代其异步函数,使其变成纯同步函数,代码如下:
const fs = require('fs')
const cache = {}
function consistentRead(filename) {
if (cache[filename]) {
return cache[filename]
} else {
// 改用同步读取文件
cache[filename] = fs.readFileSync(filename, 'utf8')
return cache[filename]
}
}
当然使用同步API代替异步API有如下注意事项:
-
用于特定功能的同步API可能并不总是可用的
-
同步API将阻塞事件循环,打破了JavaScript并发模型,是整个应用程序变慢。
在Node.js中很多情况下都不建议使用同步I/O, 然而,在某些情况下,这可能是最简单和最有效的方案。所以我们需要评估不同的应用场景,来选择不同的方案。
如果静态文件的数量有限,可以使用同步影响不大,在启动应用程序时,使用同步阻塞加载配置文件也是比较好的选择。如果必须一次性读取许多文件,那就不一样了。
使用异步,延迟执行
使用process.nextTick()来延迟执行以实现异步执行回调
const fs = require('fs')
const cache = {}
function inconsistentRead(filename, cb) {
if (cache[filename]) {
// 使用process.nextTick() 延迟一个函数异步执行
process.nextTick(() => cb(cache[filename]))
} else {
fs.readFile(filename, 'utf8', (err,data) => {
cache[filename] = data
cb(data)
})
}
}
process.nextTick() 的功能非常简单,就是将回调函数作为参数,并将其推到事件队列的顶部,在任何等待处理的I/O事件之前返回,一旦事件循环再次运行,该回调将被执行。
我们依然需要注意一个问题,如果在递归调用process.nextTick() 的时候,会导致I/O饥饿的问题,这个情况下,我们可以采用setImmediate()来替代。他们的作用虽然非常相似,但是语义却完全不同,process.nextTick() 延迟回调在任何其他I/O事件触发之前运行,setImmediate()的回调执行将在队列中已有的任何I/O事件之后排队。