Node定时器、事件循环及不同任务执行顺序 桃扇骨 2022-05-16 15:54 145阅读 0赞 ## 同异步任务执行顺序 ## **同步任务和异步任务** 同步任务总是比异步任务更早执行 **本轮循环和次轮循环** node规定: > process.nextTick和Promise的回调函数,为追加在本轮循环的异步任务(微任务队列),即同步任务一旦执行完成,就开始执行它们 > setTimeout、setInterval、setImmediate的回调函数,为追加在次轮循环的异步任务(宏任务队列)。 > 本轮循环一定早于次轮循环执行,且只有微任务队列清空了,才会执行下一个宏任务。 > process.nextTick是所有异步任务里面最快执行的。如果希望异步任务尽可能快地执行,那就使用它。 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 **执行顺序** 1. 同步任务 2. process.nextTick() 3. 微任务 4. 宏任务 //次轮循环执行 setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); //本轮循环执行 process.nextTick(() => console.log(3)); new Promise((resolve,reject)=>{ console.log(4);//Promise内部为同步任务 resolve(); }).then(() => console.log(5)); (() => console.log(6))();//自执行,同步任务 //4 6 3 5 1 2 //set1 setTimeout(()=>{ console.log(1) //set6 new Promise((resolve,reject)=>{ console.log(2) resolve() }).then(()=>{ console.log(3) }) }) //set2 new Promise((resolve,reject)=>{ console.log(4) resolve() }).then(()=>{ console.log(6) //set5 setTimeout(()=>{ console.log(5) }) }) //set3 setTimeout(()=>{ console.log(7) }) //set4 console.log(8) //执行顺序:4,8,6,1,2,3,7,5 //微任务(set2,set6) //宏任务(set1,set3,set5) ## 事件循环(event loop) ## 当node.js启动时,它初始化事件循环,处理所提供的输入脚本,该脚本可以进行Asynsynapi调用、调度计时器或调用process.nexttick-lrb-rrb-,然后开始处理事件循环。 只有一个主线程,事件循环是在主线程上完成的。 Node开始执行脚本时,会先进行事件循环的初始化,但是这时事件循环还没有开始,会先完成下面的事情。 * 同步任务 * 发出异步请求 * 规划定时器生效的时间 * 执行process.nextTick()等等 最后,上面这些事情都干完了,事件循环就正式开始了。 **事件循环的六个阶段** 事件循环会无限次地执行,一轮又一轮。只有异步任务的回调函数队列清空了,才会停止执行。 每一轮的事件循环,分成六个阶段,这些阶段会依次执行。 1. timers 2. I/O callbacks 3. idle,prepare 4. poll 5. check 6. close callbacks 每个阶段都有一个先进先出的回调函数队列。只有一个阶段的回调函数队列清空了,该执行的回调函数都执行了,事件循环才会进入下一个阶段。 > (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) { //什么也不做 } }) 脚本进入第一轮事件循环以后,没有到期的定时器,也没有已经可以执行的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的回调函数。 */ const fs = require('fs'); fs.readFile('test.js',() => { setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); }) //2 1 //上面代码会先进入I/O callbacks阶段,然后是check阶段,最后才是timers阶段。 //因此,setImmediate才会早于setTimeout执行。 setImmediate(function(){ console.log(1); process.nextTick(function(){ console.log(2); }); }); process.nextTick(function(){ console.log(3); setImmediate(function(){ console.log(4); }) }); //3 1 4 2 //两个setImmediate在同一轮循环的同一个队列里面。只有清空了这个队列,才会进入下一个阶段。
还没有评论,来说两句吧...