修改数组奇怪行为的 Javascript 闭包返回对象

Javascript closure returning object that modifies array weird behavior

提问人:Aeternus 提问时间:3/16/2023 最后编辑:Aeternus 更新时间:3/21/2023 访问量:66

问:

我试图从《Node.js 设计模式,第 3 版》一书中了解此代码的行为发生了什么

// using esm modules
// index.mjs
import { readFile } from 'fs';

const cache = new Map()

function inconsistentRead(filename, cb) {
  console.log('INSIDE INCONSISTENT READ, filename is: ', filename)
  console.log('Cache: ', cache)
  if (cache.has(filename)) {
    console.log('filename in cache, getting from cache: ', cache)
    // invoked synchronously
    cb(cache.get(filename))
  } else {
    // async function
    console.log('running readFile from fs')
    readFile(filename, 'utf8', (err, data) => {
      console.log('inside callback passed into readFile, setting cache')
      cache.set(filename, data);
      console.log('about to call callback with data = ', data)
      cb(data);
    })
  }
}

function createFileReader(filename) {
  const listeners = []
  console.log('listeners (empty at first): ', listeners)
  inconsistentRead(filename, (value) => {
    console.log('inconsistent read callback invoked, listeners: ', listeners)
    listeners.forEach((listener) => listener(value));
  })

  return {
    onDataReady: (listener) => {
      console.log("about to push listener to listeners", listeners)
      listeners.push(listener)
      console.log('after pushing to listeners: ', listeners)
    }
  }
}

const reader1 = createFileReader('data.txt')
console.log('before reader1.ondataready')
reader1.onDataReady((data) => {
  console.log(`First call data: ${data}`);
})

以下是上述结果的输出:

listeners (empty at first):  []
INSIDE INCONSISTENT READ, filename is:  data.txt
Cache:  Map(0) {}
running readFile from fs
before reader1.ondataready
about to push listener to listeners []
after pushing to listeners:  [ [Function (anonymous)] ]
inside callback passed into readFile, setting cache
about to call callback with data =  some basic data

inconsistent read callback invoked, listeners:  [ [Function (anonymous)] ]
First call data: some basic data

我感到困惑的是 readFile 的行为。 读取文件不会调用回调,直到我们填充回调数组。我本来以为它什么时候准备好调用,但相反,直到我们推入侦听器数组后它才调用它。listenersconsole.log('inconsistent read callback invoked, listeners: ', listeners)

为什么 readFile 中的回调在填充监听器后被调用?是否有可能由于时间原因,它可以在填充之前调用回调?listeners.forEach(...)

JavaScript 节点 .js 数组回 闭包

评论

3赞 Unmitigated 3/16/2023
arr.forEach在返回之前只运行一次。它不会回到过去并再次打印。
0赞 Aeternus 3/17/2023
@Unmitigated抱歉,我试图让这段代码模仿填充数组时自动运行的其他代码,我错过了其他代码的一些细微差别,所以在工作之后,我将通过更接近原始代码的编辑来更新这个问题,并具有意外的行为
0赞 Aeternus 3/18/2023
@Unmitigated编辑了问题,请参阅更新

答:

2赞 Nikhil Mehta 3/16/2023 #1
const c = closure2()

此行调用 closure2 并返回带有 onDataReady 的对象。通过以下方式调用 onDataReady 后:

c.onDataReady('test1')

它调用 onDataReady 函数,该函数推送数组中的字符串。如果你观察文档,你会看到 array.push 在更新后返回数组元素的计数,因此你得到的输出类似于 1,2,3...

它不会调用

arr.forEach(a => { console.log('ARR ELEMENT: ', a) })

在第一次调用 closure2() 之后的任何时候,由于数组最初是空的,因此不会打印任何内容。

如果要在每次推送/添加数据后打印完整的数组,可以执行如下操作:

function closure2() {
  const arr = [];
  return {
    print: () => arr.forEach(a => {
      console.log('ARR ELEMENT: ', a)
    }),
    onDataReady: (arrElement) => arr.push(arrElement)
  }
}

const c = closure2()
c.onDataReady('test1')
c.print(); // PRINTS ["test1"]
c.onDataReady('test2')
c.print() // PRINTS ["test1", "test2"]
c.onDataReady('test3')
c.print() // PRINTS ["test1", "test2", "test3"]
c.onDataReady('test4')
c.print() // PRINTS ["test1", "test2", "test3", "test4"]

根据 OP 的编辑编辑的答案

是的,在您共享的代码中填充侦听器后,将调用回调。这就是 Javascript 的工作原理。这里没有时间问题。即使您尝试读取空文件,回调也会在稍后执行。Javascript 将首先执行所有同步代码,直到调用堆栈为空,然后查看任务队列(微任务队列和宏任务队列)中存在的回调函数。由于读取文件(即 I/O 操作)是一项宏任务,因此它将在调用堆栈为空或执行所有其他顺序同步代码后执行其回调。以下是一些有助于理解此行为的好资源。

MDN 文档

规范

好的Youtube视频演示了事件循环

评论

0赞 Aeternus 3/17/2023
谢谢!请参阅编辑,如果有足够的答案,我会接受
1赞 Nikhil Mehta 3/21/2023
@Aeternus 我已经根据您的编辑编辑了我的答案。如有不清楚之处,请告诉我。
0赞 Aeternus 3/27/2023
啊,我没有立即想到任务队列!!!真正理解这种范式的最佳方式是什么?感谢您跟进我的编辑!我非常感谢。