JavaScript는 단일 스레드에서 실행되며 비동기 작업이 특히 중요합니다. 이 기사에서는 주로 노드 타이머에 대한 관련 지식을 소개합니다. 엔진 이외의 기능을 사용하는 한 비동기 작업을 구성하려면 외부와 상호 작용해야 합니다. 비동기 작업이 너무 많기 때문에 JavaScript는 많은 비동기 구문을 제공해야 합니다.
Node의 비동기 구문은 커널과 대화할 수 있고 이를 위해 특수 라이브러리 libuv를 구축해야 하기 때문에 브라우저의 비동기 구문보다 더 복잡합니다. 이 라이브러리는 다양한 콜백 함수의 실행 시간을 담당합니다. 결국 비동기 작업은 결국 메인 스레드로 돌아가서 하나씩 실행 대기열에 추가되어야 합니다.
비동기 작업을 조정하기 위해 Node는 실제로 작업이 지정된 시간에 실행될 수 있도록 4개의 타이머를 제공합니다.
setTimeout()
setInterval()
setImmediate()
process.nextTick()
처음 두 개는 언어 표준이고, 마지막 두 개는 Node에 고유합니다. 비슷한 방식으로 쓰여지고 비슷한 기능을 갖고 있어 구별하기가 쉽지 않습니다.
아래 코드를 실행한 결과를 알려주실 수 있나요?
// 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가 다양한 타이머를 처리하는 방법, 더 광범위하게는 libuv 라이브러리가 메인 스레드에서 실행될 비동기 작업을 정렬하는 방법을 자세히 설명합니다.
1. 동기 작업과 비동기 작업
우선, 동기 작업은 항상 비동기 작업보다 먼저 실행됩니다.
이전 코드에서는 마지막 줄만 동기화 작업이므로 가장 먼저 실행됩니다.
(() => console.log(5))();
2. 현재 주기와 보조 주기
비동기 작업은 두 가지 유형으로 나눌 수 있습니다.
현재 주기에 비동기 작업 추가
두 번째 주기에 비동기 작업 추가
소위 "루프"는 이벤트 루프를 나타냅니다. 이것이 JavaScript 엔진이 비동기 작업을 처리하는 방법입니다. 이에 대해서는 나중에 자세히 설명하겠습니다. 여기서는 이 주기가 두 번째 주기보다 먼저 실행되어야 한다는 점을 이해하세요.
노드는 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));
3. process.nextTick()
process.nextTick이라는 이름은 약간 오해의 소지가 있습니다. 이 주기에서 실행되며 모든 비동기 작업 중에서 가장 빠릅니다.
노드는 모든 동기화 작업을 실행한 후 process.nextTick의 작업 대기열을 실행합니다. 따라서 다음 코드 줄은 두 번째 출력입니다.
process.nextTick(() => console.log(3));
기본적으로 비동기 작업을 최대한 빠르게 실행하려면 process.nextTick을 사용하세요.
4. 마이크로태스크
언어 사양에 따라 Promise 개체의 콜백 함수는 비동기 작업의 "마이크로태스크" 대기열에 들어갑니다.
마이크로태스크 대기열은 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보다 먼저 실행됩니다.
이 시점에서 이 주기의 실행 순서가 완료됩니다.
同步任务 process.nextTick() 微任务
5. 이벤트 루프의 개념
두 번째 루프의 실행 순서부터 시작하겠습니다. 이를 위해서는 이벤트 루프가 무엇인지 이해해야 합니다.
우선, 메인 스레드 외에 별도의 이벤트 루프 스레드가 있다고 생각하는 사람들도 있습니다. 그렇지 않습니다. 메인 스레드는 하나만 있고 이벤트 루프는 메인 스레드에서 완료됩니다.
둘째, Node가 스크립트 실행을 시작하면 먼저 이벤트 루프를 초기화하지만 이벤트 루프는 아직 시작되지 않았으므로 다음 작업이 먼저 완료됩니다.
동기 작업
비동기 요청 실행
타이머가 적용되는 시간 계획
process.nextTick() 등을 실행
마지막으로 위의 모든 작업이 완료되면 , 이벤트 루프가 공식화되었습니다.
6. 이벤트 루프의 6단계
이벤트 루프는 라운드마다 무한히 실행됩니다. 비동기 작업의 콜백 함수 대기열이 지워지는 경우에만 실행이 중지됩니다.
이벤트 루프의 각 라운드는 6단계로 나뉩니다. 이러한 단계는 순차적으로 실행됩니다.
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('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) { // 什么也不做 } });
上面代码有两个异步任务,一个是 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('fs'); fs.readFile('test.js', () => { setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); });
上面代码会先进入 I/O callbacks 阶段,然后是 check 阶段,最后才是 timers 阶段。因此,setImmediate才会早于setTimeout执行。
相关推荐:
위 내용은 노드 타이머의 상세 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!