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

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

本文关键字:路径 异步 为什么 设计模式 js 释放 zalgo Node      更新时间:2023-09-26

在我现在正在阅读的一本好书中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');时,异步文件读取是否已经完成,因此侦听器不会在尝试注册它的下一行中注册?

JavaScript 永远不会中断函数来运行不同的函数。

"文件已被读取"处理程序将排队,直到 JavaScript 事件循环空闲。

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

是的,我在阅读本书的这一部分时也有同样的感觉。"不一致的阅读看起来不错"

但是在接下来的段落中,我将解释这种同步/异步函数在使用时"可能"产生的潜在错误(因此它也无法通过)。

作为总结,在使用样本中发生的是:

在事件周期 1 中:

reader1 被创建,因为"data.txt"尚未缓存,它将在其他事件周期 N 中异步响应。

某些回调是为 Reader1 准备而订阅的。并将在循环 N 上调用。

在事件周期 N 中:读取"data.txt",并对此进行通知和缓存,因此调用 reader1 订阅的回调。

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

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

X

> 1 和 X

X> N :该错误将按照书中的说明表达:

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

X === N:嗯,这是一个边缘情况,

如果 reader2 部分首先运行将传递与 X === 1 相同的内容,但是,如果在不一致读取的"data.txt"就绪部分之后运行,那么将发生与 X> N 时相同的情况

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

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设置侦听器之前完成

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

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 中,你有纯函数与 monad,它们类似于"稍后"执行的"异步"函数。在Javascript中,这些没有明确声明。因此,这些"延迟"执行的代码可能难以调试。