在释放 zalgo 的 Node.js 设计模式中,为啥异步路径是一致的?
Posted
技术标签:
【中文标题】在释放 zalgo 的 Node.js 设计模式中,为啥异步路径是一致的?【英文标题】:In Node.js design patterns unleashing zalgo why is the asynchronous path consistent?在释放 zalgo 的 Node.js 设计模式中,为什么异步路径是一致的? 【发布时间】:2016-08-16 04:53:53 【问题描述】:在我正在阅读的这本好书NodeJs design patterns
我看到以下示例:
var fs = require('fs');
var cache = ;
function inconsistentRead(filename, callback)
if (cache[filename])
//invoked synchronously
callback(cache[filename]);
else
//asynchronous function
fs.readFile(filename, 'utf8', function(err, data)
cache[filename] = data;
callback(data);
);
然后:
function createFileReader(filename)
var listeners = [];
inconsistentRead(filename, function(value)
listeners.forEach(function(listener)
listener(value);
);
);
return
onDataReady: function(listener)
listeners.push(listener);
;
及其用法:
var reader1 = createFileReader('data.txt');
reader1.onDataReady(function(data)
console.log('First call data: ' + data);
作者说,如果项目在缓存中,则行为是同步的,如果不在缓存中,则行为是异步的。我没问题。然后他继续说我们应该同步或异步。我没问题。
我不明白的是,如果我采用异步路径,那么当执行此行 var reader1 = createFileReader('data.txt');
时,异步文件读取是否已经完成,因此侦听器 不会在尝试注册它的以下行中注册?
【问题讨论】:
【参考方案1】:javascript 永远不会中断函数来运行不同的函数。
“文件已被读取”处理程序将排队,直到 JavaScript 事件循环空闲。
【讨论】:
我不明白不能在var reader1 = createFileReader('data.txt');
之间完成文件读取//文件在这里和下一行完成读取:reader1.onDataReady(function(data)
@Jas — 理论上可以,但这没有什么区别,因为在 JS 事件循环不忙于运行它所在的函数之前,不会调用数据就绪事件处理程序.
@Jas 您的错误在于认为回调将在读取完成后立即运行。它不会。回调将在读取完成后的某个未来点运行,此时调用堆栈已清除且队列中没有其他事件。 (当另一个函数在运行时,调用堆栈是非空的,所以由完成文件读取引起的事件必须等待轮到它。)
@Quentin @Quentin 是否有可能运行var reader1 = createFileReader('data.txt');
,然后文件读取完成,然后事件循环中没有事件,然后运行下一行reader1.onDataReady(function(data)
?或者我的理论失败的事情是调用堆栈是非空的。我不明白为什么要等待调用堆栈为空?
@Jas — 您在上一条评论中引用的两行 在同一个函数中,JavaScript 事件循环将在运行 该函数 所以任何进入的事件都将被放入队列中,直到该函数(以及调用它的函数,等等一直备份堆栈)完成。【参考方案2】:
直到事件循环的当前滴答之后,异步读取操作才会调用其回调或开始发出事件,因此注册事件侦听器的同步代码将首先运行。
【讨论】:
【参考方案3】:是的,读这本书的时候我也有同感。 “inconsistentRead 看起来不错”
但在接下来的段落中,我将解释这种同步/异步函数在使用时“可能”产生的潜在错误(因此它也无法通过)。
作为总结,发生在使用的样本是:
在一个事件周期1中:
reader1 已创建,导致“data.txt”尚未缓存,它将在其他事件周期 N 中异步响应。
订阅了一些回调以供 reader1 准备就绪。并且会在第 N 周期被调用。
在事件周期 N 中: 读取“data.txt”并通知和缓存,因此调用 reader1 订阅的回调。
在事件周期 X(但 X >= 1,但 X 可能在 N 之前或之后):(可能是超时,或其他异步路径调度此) reader2 是为同一个文件“data.txt”创建的
如果发生以下情况会发生什么: X === 1 :这个bug可以用没有提到的方式表达,导致data.txt结果会尝试缓存两次,第一次读取越快,获胜。但是 reader2 会在异步响应准备好之前注册它的回调,所以它们会被调用。
X > 1 AND X
X > N : bug 会按照书中的解释来表达:
您创建 reader2(它的响应已被缓存),调用 onDataReady 导致数据被缓存(但您还没有订阅任何订阅者),然后您使用 onDataReady 订阅回调,但这会不会被再次调用。
X === N:嗯,这是一个边缘情况,如果 reader2 部分首先运行将通过与 X === 1 相同的结果,但是,如果在不一致读取的“data.txt”准备部分之后运行,则将与 X > N
时发生相同【讨论】:
【参考方案4】:我认为这个问题也可以用一个更简单的例子来说明:
let gvar = 0;
let add = (x, y, callback) => callback(x + y + gvar)
add(3,3, console.log); gvar = 3
在这种情况下,callback
在add
内部立即被调用,因此gvar
之后的更改无效:console.log(3+3+0)
另一方面,如果我们异步添加
let add2 = (x, y, callback) => setImmediate(()=>callback(x + y + gvar))
add2(3, 3, console.log); gvar = 300
因为执行顺序,gvar=300
在异步调用setImmediate
之前运行,所以结果变成console.log( 3 + 3 + 300)
在 Haskell 中,您有纯函数 vs monad,它们类似于“稍后”执行的“异步”函数。在 Javascript 中,这些没有明确声明。因此,这些“延迟”执行的代码可能难以调试。
【讨论】:
以上是关于在释放 zalgo 的 Node.js 设计模式中,为啥异步路径是一致的?的主要内容,如果未能解决你的问题,请参考以下文章
Nodejs深度探秘:event loop的本质和异步代码中的Zalgo问题