在 Node.js 释放 zalgo 的设计模式中,为什么异步路径是一致的?

In Node.js design patterns unleashing zalgo why is the asynchronous path consistent?

在我正在阅读的好书中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'); 时,异步文件读取已经完成,因此监听器 won' t被注册在下面那一行试注册了吗?

JavaScript 永远不会中断一个函数到 运行 一个不同的函数。

"file has been read" 处理程序将排队等待 JavaScript 事件循环空闲。

异步读取操作直到事件循环的当前节拍之后才会调用其回调或开始发出事件,因此注册事件侦听器的同步代码将首先运行。

是的,看了这部分书,我也有同感。 "inconsistentRead looks good"

但在接下来的段落中,我将解释这种sync/async函数"could"在使用时产生的潜在错误(因此无法通过)。

作为总结,在使用示例中发生的是:

在事件周期1中:

reader1创建,由于"data.txt"还没有缓存,会在其他事件周期N.

异步响应

为了 reader1 准备就绪,订阅了一些回调。并且会在第N个周期被调用。

事件周期N: "data.txt" 已读取并已通知并缓存,因此调用了 reader1 订阅的回调。

在事件周期X中(但X >= 1,但X可能在N之前或之后):(可能是超时,或其他异步路径安排此) reader2 是为同一个文件创建的 "data.txt"

如果出现以下情况会怎样: X === 1 : 该错误可能以未提及的方式表示,导致 data.txt 结果将尝试缓存两次,第一次读取,速度越快,将获胜。但是 reader2 会在异步响应准备好之前注册它的回调,所以它们会被调用。

X > 1 AND X < N:与 X === 1 的情况相同

X > N : 错误将如书中所述表示:

你创建了 reader2(它的响应已经被缓存),调用 onDataReady 导致数据被缓存(但你还没有订阅任何订阅者),然后你用 onDataReady 订阅回调,但是这不会再被调用。

X === N:嗯,这是一个边缘情况,如果 reader2 部分 运行 首先将通过与 X === 1 相同,但是,如果 运行 之后"data.txt" inconsistentRead 的就绪部分将与 X > N

时相同

我觉得这个问题也可以用一个更简单的例子来说明:

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,它们类似于 "async" 被执行的函数 "later"。在 Javascript 中,这些没有明确声明。所以这些 "delayed" 执行的代码可能很难调试。

这个例子对我理解这个概念更有帮助

const fs = require('fs');
const cache = {};

function inconsistentRead(filename, callback) {
    if (cache[filename]) {
        console.log("load from cache")
        callback(cache[filename]);
    } else {
        fs.readFile(filename, 'utf8', function (err, data) {
            cache[filename] = data;
            callback(data);
        });
    }
}

function createFileReader(filename) {
    const listeners = [];
    inconsistentRead(filename, function (value) {
        console.log("inconsistentRead CB")
        listeners.forEach(function (listener) {
            listener(value);
        });
    });
    return {
        onDataReady: function (listener) {
            console.log("onDataReady")
            listeners.push(listener);
        }
    };
}

const reader1 = createFileReader('./data.txt');
reader1.onDataReady(function (data) {
    console.log('First call data: ' + data);
})

setTimeout(function () {
    const reader2 = createFileReader('./data.txt');
    reader2.onDataReady(function (data) {
        console.log('Second call data: ' + data);
    })
}, 100)

输出:

╰─ node zalgo.js        
onDataReady
inconsistentRead CB
First call data: :-)
load from cache
inconsistentRead CB
onDataReady

当调用是异步的时,onDataReady 处理程序在读取文件之前设置,而在异步中,迭代在 onDataReady 设置侦听器之前完成