在释放 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

在这种情况下,callbackadd 内部立即被调用,因此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 设计模式中,为啥异步路径是一致的?的主要内容,如果未能解决你的问题,请参考以下文章

如何检查字符串是不是包含 zalgo 文本?

Nodejs深度探秘:event loop的本质和异步代码中的Zalgo问题

Nodejs深度探秘:event loop的本质和异步代码中的Zalgo问题

Node.js中的内存泄漏分析

node.js适合做啥网站

如何从Error中释放localhost:listen EADDRINUSE