이 글은 Nodejs의 이벤트 루프를 이해하는 데 도움이 되도록 그림과 텍스트를 조합하여 사용합니다.
다음 전문은 7,000자입니다. 생각이 명확하고 에너지가 넘칠 때 읽어주세요. 이해하신 후에도 오랫동안 잊지 않으실 것이라고 장담합니다. [추천학습: "nodejs Tutorial"]
Nodelibuv 하단에 사용된 언어는 C++ 언어입니다. 이는 기본 운영 체제를 작동하는 데 사용되며 운영 체제의 인터페이스를 캡슐화합니다. Node의 이벤트 루프도 libuv
로 작성되어 있으므로 Node의 수명주기는 여전히 브라우저의 수명주기와 다릅니다.
Node는 운영 체제를 다루기 때문에 이벤트 루프는 상대적으로 복잡하고 고유한 API를 가지고 있습니다.
이벤트 루프는 운영 체제마다 미묘한 차이가 있습니다. 여기에는 당분간 나열되지 않은 운영 체제에 대한 지식이 포함됩니다.
이번에는 JS 메인 스레드에서 Node의 동작 과정만 소개합니다. Node의 다른 스레드는 당분간 확장되지 않습니다.
좋은 그림이고 아무것도 팔지 않습니다. 아래 그림을 이해하고 나면 이벤트 루프를 배우게 됩니다.
이벤트 루프 다이어그램
이벤트 루프 다이어그램 - 구조
모든 사람에게 전체 보기를 제공하기 위해 디렉토리 구조 다이어그램을 앞에 게시하겠습니다.
Directory
계속 자세히 얘기해보자
메인 스레드
위 그림에서 여러 색상 블록의 의미:
main
: 항목 시작 파일, 실행 메인 기능main
:启动入口文件,运行主函数event loop
:检查是否要进入事件循环over
이벤트 루프
: 이벤트 루프 진입 여부 확인over
: 모든 것이 끝났습니다. 끝났습니다
이벤트 루프 원
유휴 prepare
check: 단계 확인(setImmediate를 사용하는 콜백은 이 대기열에 직접 입력됩니다.)
🎜🎜🎜🎜 콜백 닫기🎜 🎜🎜🎜🎜🎜🎜이번에는 위에서 빨간색으로 표시된 세 가지 핵심 사항에만 중점을 둡니다. 🎜timers
、pending callback
、idle prepare
等执行完毕后,到达poll
확인 과정: 각 타이머를 순차적으로 계산하고, 타이머가 카운트를 시작한 시간부터 현재 시간까지의 시간이 타이머 간격 매개변수 설정을 충족하는지 계산합니다(예: 1000ms, 타이머가 시작된 시간부터 1m가 있는지 계산). 지금까지 계산) ). 타이머 확인이 통과되면 콜백 함수가 실행됩니다.
setTimeout(() => { console.log('object'); }, 5000) console.log('node');
setTimeout(function t1() { console.log('setTimeout'); }, 5000) console.log('node 生命周期'); const http = require('http') const server = http.createServer(function h1() { console.log('请求回调'); }); server.listen(8080)코드 분석은 다음과 같습니다.
注意:下图中的“是否有任务”的说法表示“是否有本队列的任务”。
event loop流程梳理
const startTime = new Date(); setTimeout(function f1() { console.log('setTimeout', new Date(), new Date() - startTime); }, 200) console.log('node 生命周期', startTime); const fs = require('fs') fs.readFile('./poll.js', 'utf-8', function fsFunc(err, data) { const fsTime = new Date() console.log('fs', fsTime); while (new Date() - fsTime < 300) { } console.log('结束死循环', new Date()); });
连续运行三遍,打印结果如下:
执行流程解析:
执行全局上下文,打印「node 生命周期 + 时间」
询问是否有event loop
有,进入timers队列,检查没有计时器(cpu处理速度可以,这时还没到200ms)
轮询进入到poll,读文件还没读完(比如此时才用了20ms),因此poll队列是空的,也没有任务回调
在poll队列等待……不断轮询看有没有回调
文件读完,poll队列有了fsFunc回调函数,并且被执行,输出「fs + 时间」
在while死循环那里卡300毫秒,
死循环卡到200ms的时候,f1回调进入timers队列。但此时poll队列很忙,占用了线程,不会向下执行。
直到300ms后poll队列清空,输出「结束死循环 + 时间」
event loop赶紧向下走
再来一轮到timers,执行timers队列里的f1回调。于是看到「setTimeout + 时间」
timers队列清空,回到poll队列,没有任务,等待一会。
等待时间够长后,向下回到event loop。
event loop检查没有其他异步任务了,结束线程,整个程序over退出。
检查阶段(使用 setImmediate 的回调会直接进入这个队列)
真正的队列,里边扔的就是待执行的回调函数的集合。类似[fn,fn]这种形式的。
每次到达check这个队列后,立即按顺序执行回调函数即可【类似于[fn1,fn2].forEach((fn)=>fn())的感觉】
所以说,setImmediate不是一个计时器的概念。
如果你去面试,涉及到Node环节,可能会遇到下边这个问题:setImmediate和setTimeout(0)谁更快。
check
队列,setTimeout回调在timers
队列(概念意义,实际在计时器线程,只是setTimeout在timers队列做检查调用而已。详细看timers的工作原理)。观察以下代码:
setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); });
多次反复运行,执行效果如下:
顺序不定
可以看到多次运行,两句console.log打印的顺序不定。
这是因为setTimeout的间隔数最小填1,虽然下边代码填了0。但实际计算机执行当1ms算。(这里注意和浏览器的计时器区分。在浏览器中,setInterval的最小间隔数为10ms,小于10ms则会被设置为10;设备供电状态下,间隔最小为16.6ms。)
以上代码,主线程运行的时候,setTimeout函数调用,计时器线程增加一个定时器任务。setImmediate函数调用后,其回调函数立即push到check队列。主线程执行完毕。
eventloop判断时,发现timers和check队列有内容,进入异步轮询:
第一种情况:等到了timers里这段时间,可能还没有1ms的时间,定时器任务间隔时间的条件不成立所以timers里还没有回调函数。继续向下到了check队列里,这时候setImmediate的回调函数早已等候多时,直接执行。而再下次eventloop到达timers队列,定时器也早已成熟,才会执行setTimeout的回调任务。于是顺序就是「setImmediate -> setTimeout」。
第二种情况:但也有可能到了timers阶段时,超过了1ms。于是计算定时器条件成立,setTimeout的回调函数被直接执行。eventloop再向下到达check队列执行setImmediate的回调。最终顺序就是「setTimeout -> setImmediate」了。
所以,只比较这两个函数的情况下,二者的执行顺序最终结果取决于当下计算机的运行环境以及运行速度。
------------------setTimeout测试:------------------- let i = 0; console.time('setTimeout'); function test() { if (i < 1000) { setTimeout(test, 0) i++ } else { console.timeEnd('setTimeout'); } } test(); ------------------setImmediate测试:------------------- let i = 0; console.time('setImmediate'); function test() { if (i < 1000) { setImmediate(test) i++ } else { console.timeEnd('setImmediate'); } } test();
运行观察时间差距:
setTimeout与setImmediate时间差距
可见setTimeout远比setImmediate耗时多得多
这是因为setTimeout不仅有主代码执行的时间消耗。还有在timers队列里,对于计时器线程中各个定时任务的计算时间。
如果你看懂了上边的事件循环图,下边这道题难不倒你!
// 说说下边代码的执行顺序,先打印哪个? const fs = require('fs') fs.readFile('./poll.js', () => { setTimeout(() => console.log('setTimeout'), 0) setImmediate(() => console.log('setImmediate')) })
上边这种代码逻辑,不管执行多少次,肯定都是先执行setImmediate。
先执行setImmediate
因为fs各个函数的回调是放在poll队列的。当程序holding在poll队列后,出现回调立即执行。
回调内执行setTimeout和setImmediate的函数后,check队列立即增加了回调。
回调执行完毕,轮询检查其他队列有内容,程序结束poll队列的holding向下执行。
check是poll阶段的紧接着的下一个。所以在向下的过程中,先执行check阶段内的回调,也就是先打印setImmediate。
到下一轮循环,到达timers队列,检查setTimeout计时器符合条件,则定时器回调被执行。
说完宏任务,接下来说下微任务
process.nextTick(() => {})
Promise.resolve().then(() => {})
事件循环中,每执行一个回调前,先按序清空一次nextTick和promise。
// 先思考下列代码的执行顺序 setImmediate(() => { console.log('setImmediate'); }); process.nextTick(() => { console.log('nextTick 1'); process.nextTick(() => { console.log('nextTick 2'); }) }) console.log('global'); Promise.resolve().then(() => { console.log('promise 1'); process.nextTick(() => { console.log('nextTick in promise'); }) })
最终顺序:
global
nextTick 1
nextTick 2
promise 1
nextTick in promise
setImmediate
基于上边的说法,有两个问题待思考和解决:
每走一个异步宏任务队列就查一遍nextTick和promise?还是每执行完 宏任务队列里的一个回调函数就查一遍呢?
如果在poll的holding阶段,插入一个nextTick或者Promise的回调,会立即停止poll队列的holding去执行回调吗?
上边两个问题,看下边代码的说法
setTimeout(() => { console.log('setTimeout 100'); setTimeout(() => { console.log('setTimeout 100 - 0'); process.nextTick(() => { console.log('nextTick in setTimeout 100 - 0'); }) }, 0) setImmediate(() => { console.log('setImmediate in setTimeout 100'); process.nextTick(() => { console.log('nextTick in setImmediate in setTimeout 100'); }) }); process.nextTick(() => { console.log('nextTick in setTimeout100'); }) Promise.resolve().then(() => { console.log('promise in setTimeout100'); }) }, 100) const fs = require('fs') fs.readFile('./1.poll.js', () => { console.log('poll 1'); process.nextTick(() => { console.log('nextTick in poll ======'); }) }) setTimeout(() => { console.log('setTimeout 0'); process.nextTick(() => { console.log('nextTick in setTimeout'); }) }, 0) setTimeout(() => { console.log('setTimeout 1'); Promise.resolve().then(() => { console.log('promise in setTimeout1'); }) process.nextTick(() => { console.log('nextTick in setTimeout1'); }) }, 1) setImmediate(() => { console.log('setImmediate'); process.nextTick(() => { console.log('nextTick in setImmediate'); }) }); process.nextTick(() => { console.log('nextTick 1'); process.nextTick(() => { console.log('nextTick 2'); }) }) console.log('global ------'); Promise.resolve().then(() => { console.log('promise 1'); process.nextTick(() => { console.log('nextTick in promise'); }) }) /** 执行顺序如下 global ------ nextTick 1 nextTick 2 promise 1 nextTick in promise setTimeout 0 // 解释问题1. 没有上边的nextTick和promise,setTimeout和setImmediate的顺序不一定,有了以后肯定是0先开始。 // 可见,执行一个队列之前,就先检查并执行了nextTick和promise微队列 nextTick in setTimeout setTimeout 1 nextTick in setTimeout1 promise in setTimeout1 setImmediate nextTick in setImmediate poll 1 nextTick in poll ====== setTimeout 100 nextTick in setTimeout100 promise in setTimeout100 setImmediate in setTimeout 100 nextTick in setImmediate in setTimeout 100 setTimeout 100 - 0 nextTick in setTimeout 100 - 0 */
以上代码执行多次,顺序不变,setTimeout和setImmediate的顺序都没变。
执行顺序及具体原因说明如下:
global
:主线程同步任务,率先执行没毛病
nextTick 1
:执行异步宏任务之前,清空异步微任务,nextTick优先级高,先行一步
nextTick 2
:执行完上边这句代码,又一个nextTick微任务,立即率先执行
promise 1
:执行异步宏任务之前,清空异步微任务,Promise的优先级低,所以在nextTick完了以后立即执行
nextTick in promise
:清空Promise队列的过程中,遇到nextTick微任务,立即执行、清空
setTimeout 0
: 解释第一个问题. 没有上边的nextTick和promise,只有setTimeout和setImmediate时他俩的执行顺序不一定。有了以后肯定是0先开始。可见,执行一个宏队列之前,就先按顺序检查并执行了nextTick和promise微队列。等微队列全部执行完毕,setTimeout(0)的时机也成熟了,就被执行。
nextTick in setTimeout
:执行完上边这句代码,又一个nextTick微任务,立即率先执行 【这种回调函数里的微任务,我不能确定是紧随同步任务执行的;还是放到微任务队列,等下一个宏任务执行前再清空的他们。但是顺序看上去和立即执行他们一样。不过我比较倾向于是后者:先放到微任务队列等待,下一个宏任务执行前清空他们。】
setTimeout 1
:因为执行微任务耗费时间,导致此时timers里判断两个0和1的setTimeout计时器已经结束,所以两个setTimeout回调都已加入队列并被执行
nextTick in setTimeout1
:执行完上边这句代码,又一个nextTick微任务,立即率先执行 【可能是下一个宏任务前清空微任务】
promise in setTimeout1
:执行完上边这句代码,又一个Promise微任务,立即紧随执行 【可能是下一个宏任务前清空微任务】
setImmediate
:poll队列回调时机未到,先行向下到check队列,清空队列,立即执行setImmediate回调
nextTick in setImmediate
:执行完上边这句代码,又一个nextTick微任务,立即率先执行 【可能是下一个宏任务前清空微任务】
poll 1
:poll队列实际成熟,回调触发,同步任务执行。
nextTick in poll
:执行完上边这句代码,又一个nextTick微任务,立即率先执行 【可能是下一个宏任务前清空微任务】
setTimeout 100
:定时器任务到达时间,执行回调。并在回调里往微任务推入了nextTick、Promise,往宏任务的check里推入了setImmediate的回调。并且也开启了计时器线程,往timers里增加了下一轮回调的可能。
nextTick in setTimeout100
:宏任务向下前,率先执行定时器回调内新增的微任务-nextTick 【这里就能确定了,是下一个宏任务前清空微任务的流程】
promise in setTimeout100
:紧接着执行定时器回调内新增的微任务-Promise 【清空完nextTick清空Promise的顺序】
setImmediate in setTimeout 100
:这次setImmediate比setTimeout(0)先执行的原因是:流程从timers向后走到check队列,已经有了setImmediate的回调,立即执行。
nextTick in setImmediate in setTimeout 100
:执行完上边这句代码,又一个nextTick微任务,下一个宏任务前率先清空微任务
setTimeout 100 - 0
:轮询又一次回到timers,执行100-0的回调。
nextTick in setTimeout 100 - 0
:执行完上边这句代码,又一个nextTick微任务,下一个宏任务前率先清空微任务。
一开始设计的时候,setImmediate充当了微队列的作用(虽然他不是)。设计者希望执行完poll后立即执行setImmediate(当然现在也确实是这么表现的)。所以起的名字叫Immediate
,表示立即
的意思。
但是后来问题是,poll里可能有N个任务连续执行,在执行期间想要执行setImmediate是不可能的。因为poll队列不停,流程不向下执行。
于是出现nextTick,真正的微队列概念。但此时,immediate的名字被占用了,所以名字叫nextTick(下一瞬间)。事件循环期间,执行任何一个队列之前,都要检查他是否被清空。其次是Promise。
最后,检验学习成果的面试题来了
async function async1() { console.log('async start'); await async2(); console.log('async end'); } async function async2(){ console.log('async2'); } console.log('script start'); setTimeout(() => { console.log('setTimeout 0'); }, 0) setTimeout(() => { console.log('setTimeout 3'); }, 3) setImmediate(() => { console.log('setImmediate'); }) process.nextTick(() => { console.log('nextTick'); }) async1(); new Promise((res) => { console.log('promise1'); res(); console.log('promise2'); }).then(() => { console.log('promise 3'); }); console.log('script end'); // 答案如下 // - // - // - // - // - // - // - // - // - // - // - // - /** script start async start async2 promise1 promise2 script end nextTick async end promise 3 // 后边这仨的运行顺序就是验证你电脑运算速度的时候了。 速度最好(执行上边的同步代码 + 微任务 + 计时器运算用了不到0ms): setImmediate setTimeout 0 setTimeout 3 速度中等(执行上边的同步代码 + 微任务 + 计时器运算用了0~3ms以上): setTimeout 0 setImmediate setTimeout 3 速度较差(执行上边的同步代码 + 微任务 + 计时器运算用了3ms以上): setTimeout 0 setTimeout 3 setImmediate */
更多编程相关知识,请访问:编程视频!!
위 내용은 그림과 텍스트의 조합은 Nodejs의 이벤트 루프를 이해하는 데 도움이 됩니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!