>  기사  >  웹 프론트엔드  >  노드 타이머 지식의 상세한 해석

노드 타이머 지식의 상세한 해석

亚连
亚连원래의
2018-06-04 15:58:051526검색

이 글은 주로 노드 타이머 관련 지식을 소개합니다. 매우 훌륭하고 참고할 가치가 있습니다. 도움이 필요한 친구가 참고할 수 있습니다.

JavaScript는 단일 스레드에서 실행되며 비동기 작업이 특히 중요합니다.

엔진 외부의 기능을 사용하는 한 외부와 상호 작용해야 하므로 비동기 작업이 형성됩니다. 비동기 작업이 너무 많기 때문에 JavaScript는 많은 비동기 구문을 제공해야 합니다. 마치 어떤 사람들은 항상 맞고, 타격에 저항하는 능력이 더 강해져야 하는 것과 같습니다. 그렇지 않으면 그들은 끝장날 것입니다.

Node의 비동기 구문은 커널과 대화할 수 있고 이를 위해 특수 라이브러리 libuv를 구축해야 하기 때문에 브라우저의 비동기 구문보다 더 복잡합니다. 이 라이브러리는 다양한 콜백 함수의 실행 시간을 담당합니다. 결국 비동기 작업은 결국 메인 스레드로 돌아가서 하나씩 실행 대기열에 추가되어야 합니다.

비동기 작업을 조정하기 위해 Node는 실제로 작업이 지정된 시간에 실행될 수 있도록 4개의 타이머를 제공합니다.

  • settimeout ()

  • setinterval ()

  • setimmediate ()

  • process.nexttick ()

첫 번째 두 가지는 언어 표준이고 마지막 두 가지는 노드에 고유합니다. 비슷한 방식으로 쓰여지고 비슷한 기능을 갖고 있어 구별하기가 쉽지 않습니다.

아래 코드를 실행한 결과를 알려주실 수 있나요?

// test.js
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
(() => console.log(5))();

실행 결과는 다음과 같습니다.

$ node test.js$ node test.js

如果你能一口说对,可能就不需要再看下去了。本文详细解释,Node 怎么处理各种定时器,或者更广义地说,libuv 库怎么安排异步任务在主线程上执行。

一、同步任务和异步任务

首先,同步任务总是比异步任务更早执行。

前面的那段代码,只有最后一行是同步任务,因此最早执行。

(() => console.log(5))();

二、本轮循环和次轮循环

异步任务可以分成两种。

追加在本轮循环的异步任务
追加在次轮循环的异步任务

所谓”循环”,指的是事件循环(event loop)。这是 JavaScript 引擎处理异步任务的方式,后文会详细解释。这里只要理解,本轮循环一定早于次轮循环执行即可。

Node 规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。

这就是说,文首那段代码的第三行和第四行,一定比第一行和第二行更早执行。

// 下面两行,次轮循环执行
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
// 下面两行,本轮循环执行
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));

三、process.nextTick()

process.nextTick这个名字有点误导,它是在本轮循环执行的,而且是所有异步任务里面最快执行的。

Node 执行完所有同步任务,接下来就会执行process.nextTick的任务队列。所以,下面这行代码是第二个输出结果。

process.nextTick(() => console.log(3));

基本上,如果你希望异步任务尽可能快地执行,那就使用process.nextTick。

四、微任务

根据语言规格,Promise对象的回调函数,会进入异步任务里面的”微任务”(microtask)队列。

微任务队列追加在process.nextTick队列的后面,也属于本轮循环。所以,下面的代码总是先输出3,再输出4。

process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 3
// 4

注意,只有前一个队列全部清空以后,才会执行下一个队列。

process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 1
// 3
// 2
// 4

上面代码中,全部process.nextTick的回调函数,执行都会早于Promise

한 문장으로 바로 이해할 수 있다면 더 이상 읽을 필요가 없을 수도 있습니다. 이 기사에서는 Node가 다양한 타이머를 처리하는 방법, 더 광범위하게는 libuv 라이브러리가 메인 스레드에서 실행될 비동기 작업을 정렬하는 방법을 자세히 설명합니다.

1. 동기 작업과 비동기 작업

우선 동기 작업은 항상 비동기 작업보다 먼저 실행됩니다.

이전 코드에서는 마지막 줄만 동기화 작업이므로 가장 먼저 실행됩니다.

(() => console.log(5))();

2. 현재 주기와 보조 주기

🎜🎜비동기 작업은 두 가지 유형으로 나눌 수 있습니다. 🎜🎜현재 주기에 비동기 작업 추가
두 번째 주기에 비동기 작업 추가🎜🎜소위 "루프"는 이벤트 루프를 나타냅니다. 이것이 JavaScript 엔진이 비동기 작업을 처리하는 방법입니다. 이에 대해서는 나중에 자세히 설명하겠습니다. 여기서는 이 주기가 두 번째 주기보다 먼저 실행되어야 한다는 점을 이해하세요. 🎜🎜노드는 process.nextTick 및 Promise의 콜백 함수가 이 주기에 추가되도록 규정합니다. 즉, 동기화 작업이 완료되면 실행됩니다. 두 번째 주기에는 setTimeout, setInterval, setImmediate의 콜백 함수가 추가됩니다. 🎜🎜이는 글 시작 부분의 코드 중 세 번째와 네 번째 줄이 첫 번째와 두 번째 줄보다 먼저 실행되어야 한다는 의미입니다. 🎜
同步任务
process.nextTick()
微任务
🎜🎜🎜3. process.nextTick()🎜🎜🎜🎜 process.nextTick이라는 이름은 약간 오해의 소지가 있으며 이 주기에서 실행되며 모든 비동기 작업 중에서 가장 빠릅니다. 🎜🎜🎜🎜Node가 모든 동기화를 완료했습니다. 작업이 완료되면 process.nextTick의 작업 대기열이 다음에 실행됩니다. 따라서 다음 코드 줄은 두 번째 출력입니다. 🎜
const fs = require('fs');
const timeoutScheduled = Date.now();
// 异步任务一:100ms 后执行的定时器
setTimeout(() => {
 const delay = Date.now() - timeoutScheduled;
 console.log(`${delay}ms`);
}, 100);
// 异步任务二:至少需要 200ms 的文件读取
fs.readFile('test.js', () => {
 const startCallback = Date.now();
 while (Date.now() - startCallback < 200) {
 // 什么也不做
 }
});
🎜기본적으로 비동기 작업을 최대한 빠르게 실행하려면 process.nextTick을 사용하세요. 🎜🎜🎜🎜4. Microtasks🎜🎜🎜🎜언어 사양에 따라 Promise 개체의 콜백 함수는 "microtask"(microtask) 대기열에 들어갑니다. 비동기 작업 . 🎜🎜마이크로태스크 대기열은 process.nextTick 대기열 뒤에 추가되며 이 주기에도 속합니다. 따라서 다음 코드는 항상 3을 먼저 출력한 다음 4를 출력합니다. 🎜
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
🎜🎜🎜참고, first 모든 큐가 지워진 후 다음 큐가 실행됩니다. 🎜
const fs = require(&#39;fs&#39;);
fs.readFile(&#39;test.js&#39;, () => {
 setTimeout(() => console.log(1));
 setImmediate(() => console.log(2));
});
🎜위 코드에서는 process.nextTick의 모든 콜백 함수가 Promise보다 먼저 실행됩니다. 🎜🎜이 시점에서 이 주기의 실행 순서가 완료됩니다. 🎜rrreee🎜🎜🎜 5. 이벤트 루프의 개념 🎜🎜🎜🎜 두 번째 루프의 실행 순서부터 시작하겠습니다. 이를 위해서는 이벤트 루프가 무엇인지 이해해야 합니다. 🎜🎜Node의 공식 문서에는 이렇게 소개되어 있습니다. 🎜🎜“Node.js가 시작되면 이벤트 루프를 초기화하고 비동기 API 호출, 타이머 예약 또는 process.nextTick() 호출을 할 수 있는 제공된 입력 스크립트를 처리한 다음 이벤트 루프 처리를 시작합니다.”🎜🎜이 단락 단어는 중요하므로 주의 깊게 읽어야 합니다. 이는 세 가지 수준의 의미를 표현합니다. 🎜🎜우선, 메인 스레드 외에 별도의 이벤트 루프 스레드가 있다고 생각하는 사람들도 있습니다. 그렇지 않습니다. 메인 스레드는 하나만 있고 이벤트 루프는 메인 스레드에서 완료됩니다. 🎜

其次,Node 开始执行脚本时,会先进行事件循环的初始化,但是这时事件循环还没有开始,会先完成下面的事情。

  • 同步任务

  • 发出异步请求

  • 规划定时器生效的时间

执行process.nextTick()等等

最后,上面这些事情都干完了,事件循环就正式开始了。

六、事件循环的六个阶段

事件循环会无限次地执行,一轮又一轮。只有异步任务的回调函数队列清空了,才会停止执行。

每一轮的事件循环,分成六个阶段。这些阶段会依次执行。

timers
I/O callbacks
idle, prepare
poll
check
close callbacks

每个阶段都有一个先进先出的回调函数队列。只有一个阶段的回调函数队列清空了,该执行的回调函数都执行了,事件循环才会进入下一个阶段。

下面简单介绍一下每个阶段的含义,详细介绍可以看官方文档,也可以参考 libuv 的源码解读。

(1)timers

这个是定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。

(2)I/O callbacks

除了以下操作的回调函数,其他的回调函数都在这个阶段执行。

  • setTimeout()和setInterval()的回调函数

  • setImmediate()的回调函数

  • 用于关闭请求的回调函数,比如socket.on('close', ...)

(3)idle, prepare

该阶段只供 libuv 内部调用,这里可以忽略。

(4)Poll

这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。

这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。

(5)check

该阶段执行setImmediate()的回调函数。

(6)close callbacks

该阶段执行关闭请求的回调函数,比如socket.on('close', ...)

七、事件循环的示例

下面是来自官方文档的一个示例。

const fs = require(&#39;fs&#39;);
const timeoutScheduled = Date.now();
// 异步任务一:100ms 后执行的定时器
setTimeout(() => {
 const delay = Date.now() - timeoutScheduled;
 console.log(`${delay}ms`);
}, 100);
// 异步任务二:至少需要 200ms 的文件读取
fs.readFile(&#39;test.js&#39;, () => {
 const startCallback = Date.now();
 while (Date.now() - startCallback < 200) {
 // 什么也不做
 }
});

上面代码有两个异步任务,一个是 100ms 后执行的定时器,一个是至少需要 200ms 的文件读取。请问运行结果是什么?

脚本进入第一轮事件循环以后,没有到期的定时器,也没有已经可以执行的 I/O 回调函数,所以会进入 Poll 阶段,等待内核返回文件读取的结果。由于读取小文件一般不会超过 100ms,所以在定时器到期之前,Poll 阶段就会得到结果,因此就会继续往下执行。

第二轮事件循环,依然没有到期的定时器,但是已经有了可以执行的 I/O 回调函数,所以会进入 I/O callbacks 阶段,执行fs.readFile的回调函数。这个回调函数需要 200ms,也就是说,在它执行到一半的时候,100ms 的定时器就会到期。但是,必须等到这个回调函数执行完,才会离开这个阶段。

第三轮事件循环,已经有了到期的定时器,所以会在 timers 阶段执行定时器。最后输出结果大概是200多毫秒。

八、setTimeout 和 setImmediate

由于setTimeout在 timers 阶段执行,而setImmediate在 check 阶段执行。所以,setTimeout会早于setImmediate完成。

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

上面代码应该先输出1,再输出2,但是实际执行的时候,结果却是不确定,有时还会先输出2,再输出1。

这是因为setTimeout的第二个参数默认为0。但是实际上,Node 做不到0毫秒,最少也需要1毫秒,根据官方文档,第二个参数的取值范围在1毫秒到2147483647毫秒之间。也就是说,setTimeout(f, 0)等同于setTimeout(f, 1)

实际执行的时候,进入事件循环以后,有可能到了1毫秒,也可能还没到1毫秒,取决于系统当时的状况。如果没到1毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行setImmediate的回调函数。

但是,下面的代码一定是先输出2,再输出1。

const fs = require(&#39;fs&#39;);
fs.readFile(&#39;test.js&#39;, () => {
 setTimeout(() => console.log(1));
 setImmediate(() => console.log(2));
});

上面代码会先进入 I/O callbacks 阶段,然后是 check 阶段,最后才是 timers 阶段。因此,setImmediate才会早于setTimeout执行。

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

JS实现的集合去重,交集,并集,差集功能示例

Bootstrap 中data-[*] 属性的整理

基于Axios 常用的请求方法别名(详解)

위 내용은 노드 타이머 지식의 상세한 해석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.