关于前端:深入研究-Nodejs-的回调队列

48次阅读

共计 5797 个字符,预计需要花费 15 分钟才能阅读完成。

作者:Dillion Megida

翻译:疯狂的技术宅

原文:https://blog.logrocket.com/a-…

未经容许严禁转载

队列是 Node.js 中用于无效解决异步操作的一项重要技术。

在本文中,咱们将深入研究 Node.js 中的队列:它们是什么,它们如何工作(通过事件循环)以及它们的类型。

Node.js 中的队列是什么?

队列是 Node.js 中用于组织异步操作的数据结构。这些操作以不同的模式存在,包含 HTTP 申请、读取或写入文件操作、流等。

在 Node.js 中解决异步操作十分具备挑战性。

HTTP 申请期间可能会呈现不可预测的提早(或者更蹩脚的可能性是没有后果),具体取决于网络品质。尝试用 Node.js 读写文件时也有可能会产生提早,具体取决于文件的大小。

相似于计时器和其余的许多操作,异步操作实现的工夫也有可能是不确定的。

在这些不同的提早状况之下,Node.js 须要可能无效地解决所有这些操作。

Node.js 无奈解决基于 first-start-first-handle(先开始先解决)或 first-finish-first-handle(先完结先解决)的操作。

之所以不能这样做的一个起因是,在一个异步操作中可能还会蕴含另一个异步操作。

为第一个异步过程留出空间意味着必须先要实现外部异步过程,而后能力思考队列中的其余异步操作。

有许多状况须要思考,因而最好的抉择是制订规定。这个规定影响了事件循环和队列在 Node.js 中的工作形式。

让咱们简要地看一下 Node.js 是怎么解决异步操作的。

调用栈,事件循环和回调队列

调用栈被用于跟踪以后正在执行的函数以及从何处开始运行。当一个函数将要执行时,它会被增加到调用堆栈中。这有助于 JavaScript 在执行函数后从新跟踪其解决步骤。

回调队列是在后盾操作实现时把回调函数保留为异步操作的队列。它们以先进先出(FIFO)的形式工作。咱们将会在本文前面介绍不同类型的回调队列。

请留神,Node.js 负责所有异步流动,因为 JavaScript 能够利用其单线程性质来阻止产生新的线程。

在实现后盾操作后,它还负责向回调队列增加函数。JavaScript 自身与回调队列无关。同时事件循环会间断查看调用栈是否为空,以便能够从回调队列中提取一个函数并增加到调用栈中。事件循环仅在执行所有同步操作之后才查看队列。

那么,事件循环是依照什么样的程序从队列中抉择回调函数的呢?

首先,让咱们看一下回调队列的五种次要类型。

回调队列的类型

IO 队列(IO queue)

IO 操作是指波及外部设备(如计算机的硬盘、网卡等)的操作。常见的操作包含读写文件操作、网络操作等。这些操作应该是异步的,因为它们留给 Node.js 解决。

JavaScript 无法访问计算机的外部设施。当执行此类操作时,JavaScript 会将其传输到 Node.js 以在后盾解决。

实现后,它们将会被转移到 IO 回调队列中,来进行事件循环,以转移到调用栈中执行。

计时器队列(Timer queue)

每个波及 Node.js 计时器性能的操作(如 setTimeout()setInterval())都是要被增加到计时器队列的。

请留神,JavaScript 语言自身没有计时器性能。它应用 Node.js 提供的计时器 API(包含 setTimeout)执行与工夫相干的操作。所以计时器操作是异步的。无论是 2 秒还是 0 秒,JavaScript 都会把与工夫相干的操作移交给 Node.js,而后将其实现并增加到计时器队列中。

例如:

setTimeout(function() {console.log('setTimeout');
    }, 0)
    console.log('yeah')


# 返回
yeah
setTimeout

在解决异步操作时,JavaScript 会继续执行其余操作。只有在所有同步操作都已被处理完毕后,事件循环才会进入回调队列。

微工作队列(Microtask queue)

该队列分为两个队列:

  • 第一个队列蕴含因 process.nextTick 函数而提早的函数。

事件循环执行的每个迭代称为一个 tick(工夫刻度)。

process.nextTick 是一个函数,它在下一个 tick(即事件循环的下一个迭代)执行一个函数。微工作队列须要存储此类函数,以便能够在下一个 tick 执行它们。

这意味着事件循环必须持续查看微工作队列中的此类函数,而后再进入其余队列。

  • 第二个队列蕴含因 promises 而提早的函数。

如你所见,在 IO 和计时器队列中,所有与异步操作无关的内容都被移交给了异步函数。

然而 promise 不同。在 promise 中,初始变量存储在 JavaScript 内存中(你可能曾经留神到了<Pending>)。

异步操作实现后,Node.js 会将函数(附加到 Promise)放在微工作队列中。同时它用失去的后果来更新 JavaScript 内存中的变量,以使该函数不与 <Pending> 一起运行。

以下代码阐明了 promise 是如何工作的:

let prom = new Promise(function (resolve, reject) {
        // 提早执行
        setTimeout(function () {return resolve("hello");
        }, 2000);
    });
    console.log(prom);
    // Promise {<pending>}
    
    prom.then(function (response) {console.log(response);
    });
    // 在 2000ms 之后, 输入
    // hello

对于微工作队列,须要留神一个重要性能,事件循环在进入其余队列之前要重复查看并执行微工作队列中的函数。例如,当微工作队列实现时,或者说计时器操作执行了 Promise 操作,事件循环将会在持续进入计时器队列中的其余函数之前参加该 Promise 操作。

因而,微工作队列比其余队列具备最高的优先级。

查看队列(Check queue)

查看队列也称为即时队列(immediate queue)。IO 队列中的所有回调函数均已执行结束后,立刻执行此队列中的回调函数。setImmediate 用于向该队列增加函数。

例如:

const fs = require('fs');
setImmediate(function() {console.log('setImmediate');
})
// 假如此操作须要 1ms
fs.readFile('path-to-file', function() {console.log('readFile')
})
// 假如此操作须要 3ms
do...while...

执行该程序时,Node.js 把 setImmediate 回调函数增加到查看队列。因为整个程序尚未筹备结束,因而事件循环不会查看任何队列。

因为 readFile 操作是异步的,所以会移交给 Node.js,之后程序将会继续执行。

do while 操作继续 3ms。在这段时间内,readFile 操作实现并被推送到 IO 队列。实现此操作后,事件循环将会开始查看队列。

只管首先填充了查看队列,但只有在 IO 队列为空之后才思考应用它。所以在 setImmediate 之前,将 readFile 输入到控制台。

敞开队列(Close queue)

此队列存储与敞开事件操作关联的函数。

包含以下内容:

  • 流敞开事件,在敞开流时收回。它示意不再收回任何事件。
  • http 敞开事件,在服务器敞开时收回。

这些队列被认为是优先级最低的,因为此处的操作会在当前产生。

你肯 sing 不心愿在解决 promise 函数之前在 close 事件中执行回调函数。当服务器曾经敞开时,promise 函数会做些什么呢?

队列程序

微工作队列具备最高优先级,其次是计时器队列,I/ O 队列,查看队列,最初是敞开队列。

回调队列的例子

让咱们通过一个更简单的例子来阐明队列的类型和程序:

const fs = require("fs");

// 假如此操作须要 2ms
fs.writeFile('./new-file.json', '...', function() {console.log('writeFile')
})

// 假如这须要 10ms 能力实现 
fs.readFile("./file.json", function(err, data) {console.log("readFile");
});

// 不须要假如,这实际上须要 1ms
setTimeout(function() {console.log("setTimeout");
}, 1000);

// 假如此操作须要 3ms
while(...) {...}

setImmediate(function() {console.log("setImmediate");
});

// 解决 promise 须要 4 ms
let promise = new Promise(function (resolve, reject) {setTimeout(function () {return resolve("promise");
    }, 4000);
});
promise.then(function(response) {console.log(response)
})

console.log("last line");

程序流程如下:

  • 在 0 毫秒时,程序开始。
  • 在 Node.js 将回调函数增加到 IO 队列之前,fs.writeFile 在后盾破费 2 毫秒。

fs.readFile takes 10ms at the background before Node.js adds the callback function to the IO queue.

  • 在 Node.js 将回调函数增加到 IO 队列之前,fs.readFile 在后盾破费 10 毫秒。
  • 在 Node.js 将回调函数增加到计时器队列之前,setTimeout 在后盾破费 1ms。
  • 当初,while 操作(同步)须要 3ms。在此期间,线程被阻止(请记住 JavaScript 是单线程的)。
  • 同样在这段时间内,setTimeoutfs.writeFile 操作实现,并将它们的回调函数别离增加到计时器和 IO 队列中。

当初的队列是:

// queues
Timer = [function () {console.log("setTimeout");
    },
];
IO = [function () {console.log("writeFile");
    },
];

setImmediate 将回调函数增加到 Check 队列中:

js
// 队列
Timer...
IO...
Check = [function() {console.log("setImmediate")}
]

在将 promise 操作增加到微工作队列之前,须要破费 4ms 的工夫在后盾进行解析。

最初一行是同步的,因而将会立刻执行:

# 返回
"last line"

因为所有同步流动都已实现,所以事件循环开始查看队列。因为微工作队列为空,因而它从计时器队列开始:

// 队列
Timer = [] // 当初是空的
IO...
Check...


# 返回
"last line"
"setTimeout"

当事件循环继续执行队列中的回调函数时,promise 操作实现并被增加到微工作队列中:

// 队列
    Timer = [];
    Microtask = [function (response) {console.log(response);
        },
    ];
    IO = []; // 以后是空的
    Check = []; // 以后是在 IO 的前面,为空


    # results
    "last line"
    "setTimeout"
    "writeFile"
    "setImmediate"

几秒钟后,readFile 操作实现,并增加到 IO 队列中:

// 队列
    Timer = [];
    Microtask = []; // 以后是空的
    IO = [function () {console.log("readFile");
        },
    ];
    Check = [];


    # results
    "last line"
    "setTimeout"
    "writeFile"
    "setImmediate"
    "promise"

最初,执行所有回调函数:

// 队列
    Timer = []
    Microtask = []
    IO = [] // 当初又是空的
    Check = [];


    # results
    "last line"
    "setTimeout"
    "writeFile"
    "setImmediate"
    "promise"
    "readFile"

这里要留神的三点:

  • 异步操作取决于增加到队列之前的延迟时间。并不取决于它们在程序中的寄存程序。
  • 事件循环在每次迭代之持续查看其余工作之前,会间断查看微工作队列。
  • 即便在后盾有另一个 IO 操作(readFile),事件循环也会执行查看队列中的函数。这样做的起因是此时 IO 队列为空。请记住,在执行 IO 队列中的所有的函数之后,将会立刻运行查看队列回调。

总结

JavaScript 是单线程的。每个异步函数都由依赖操作系统外部函数工作的 Node.js 去解决。

Node.js 负责将回调函数(通过 JavaScript 附加到异步操作)增加到回调队列中。事件循环会确定将要在每次迭代中接下来要执行的回调函数。

理解队列如何在 Node.js 中工作,使你对其有了更好的理解,因为队列是环境的外围性能之一。Node.js 最受欢迎的定义是 non-blocking(非阻塞),这意味着异步操作能够被正确的解决。都是因为有了事件循环和回调队列能力使此性能失效。

前端刷题神器

扫码进入前端面试星球????,解锁刷题神器,还能够获取 800+ 道前端面试题一线常见面试高频考点


本文首发微信公众号:前端先锋

欢送扫描二维码关注公众号,每天都给你推送陈腐的前端技术文章

欢送持续浏览本专栏其它高赞文章:

  • 深刻了解 Shadow DOM v1
  • 一步步教你用 WebVR 实现虚拟现实游戏
  • 13 个帮你进步开发效率的古代 CSS 框架
  • 疾速上手 BootstrapVue
  • JavaScript 引擎是如何工作的?从调用栈到 Promise 你须要晓得的所有
  • WebSocket 实战:在 Node 和 React 之间进行实时通信
  • 对于 Git 的 20 个面试题
  • 深刻解析 Node.js 的 console.log
  • Node.js 到底是什么?
  • 30 分钟用 Node.js 构建一个 API 服务器
  • Javascript 的对象拷贝
  • 程序员 30 岁前月薪达不到 30K,该何去何从
  • 14 个最好的 JavaScript 数据可视化库
  • 8 个给前端的顶级 VS Code 扩大插件
  • Node.js 多线程齐全指南
  • 把 HTML 转成 PDF 的 4 个计划及实现

  • 更多文章 …

正文完
 0