JS的代码执行是基于一种事件循环的机制,之所以称作事件循环,MDN给出的解释为
因为它经常被用于类似如下的方式来实现
while (queue.waitForMessage()) { queue.processNextMessage(); }如果当前没有任何消息queue.waitForMessage 会等待同步消息到达
我们可以把它当成一种程序结构的模型,处理的方案。更详细的描述可以查看 这篇文章
而JS的运行环境主要有两个:浏览器、Node。
在两个环境下的Event Loop实现是不一样的,在浏览器中基于 规范 来实现,不同浏览器可能有小小区别。在Node中基于 libuv 这个库来实现
JS是单线程执行的,而基于事件循环模型,形成了基本没有阻塞(除了alert或同步XHR等操作)的状态
二、Macrotask 与 Microtask
根据 规范,每个线程都有一个事件循环(Event Loop),在浏览器中除了主要的页面执行线程 外,Web worker是在一个新的线程中运行的,所以可以将其独立看待。
每个事件循环有至少一个任务队列(Task Queue,也可以称作Macrotask宏任务),各个任务队列中放置着不同来源(或者不同分类)的任务,可以让浏览器根据自己的实现来进行优先级排序
以及一个微任务队列(Microtask Queue),主要用于处理一些状态的改变,UI渲染工作之前的一些必要操作(可以防止多次无意义的UI渲染)
主线程的代码执行时,会将执行程序置入执行栈(Stack)中,执行完毕后出栈,另外有个堆空间(Heap),主要用于存储对象及一些非结构化的数据
一开始
宏任务与微任务队列里的任务随着:任务进栈、出栈、任务出队、进队之间交替着进行
从macrotask队列中取出一个任务处理,处理完成之后(此时执行栈应该是空的),从microtask队列中一个个按顺序取出所有任务进行处理,处理完成之后进入UI渲染后续工作
需要注意的是:microtask并不是在macrotask完成之后才会触发,在回调函数之后,只要执行栈是空的,就会执行microtask。也就是说,macrotask执行期间,执行栈可能是空的(比如在冒泡事件的处理时)
然后循环继续
常见的macrotask有:
run
输出结果
requestAnimationframe是在setTimeout之前执行的,start之后并不是直接输出end,也许这两个
点击内部的inner块,会输出什么呢?
MutationObserver优先级比promise高,虽然在一开始就被定义,但实际上是触发之后才会被添加到microtask队列中,所以先输出了promise
两个timeout回调都在最后才触发,因为click事件冒泡了,事件派发这个macrotask任务包括了前后两个onClick回调,两个回调函数都执行完之后,才会执行接下来的 setTimeout任务
期间第一个onClick回调完成后执行栈为空,就马上接着执行microtask队列中的任务
如果把代码的注释去掉,使用代码自动 click(),思考一下,会输出什么?
可以看到,事件处理是同步的,done在连续输出两个click之后才输出
而mutate只有一个,是因为当前执行第二个onClick回调的时候,microtask队列中已经有一个MutationObserver,它是第一个回调的,因为事件同步的原因没有被及时执行。浏览器会对MutationObserver进行优化,不会重复添加监听回调
四、在Node中的实现在Node环境中,macrotask部分主要多了setImmediate,microtask部分主要多了process.nextTick,而这个nextTick是独立出来自成队列的,优先级高于其他microtask
不过事件循环的的实现就不太一样了,可以参考 Node事件文档 libuv事件文档
Node中的事件循环有6个阶段
timers:执行setTimeout() 和 setInterval()中到期的callback
I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行
idle, prepare:仅内部使用
poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段
check:执行setImmediate的callback
close callbacks:执行close事件的callback,例如socket.on("close",func)
每一轮事件循环都会经过六个阶段,在每个阶段后,都会执行microtask
比较特殊的是在poll阶段,执行程序同步执行poll队列里的回调,直到队列为空或执行的回调达到系统上限
接下来再检查有无预设的setImmediate,如果有就转入check阶段,没有就先查询最近的timer的距离,以其作为poll阶段的阻塞时间,如果timer队列是空的,它就一直阻塞下去
而nextTick并不在这些阶段中执行,它在每个阶段之后都会执行
看一个例子
setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4)); console.log(5);
根据以上知识,应该很快就能知道输出结果是 5 3 4 1 2
修改一下
process.nextTick(() => console.log(1)); Promise.resolve().then(() => console.log(2)); process.nextTick(() => console.log(3)); Promise.resolve().then(() => { process.nextTick(() => console.log(0)); console.log(4); });输出为 1 3 2 4 0,因为nextTick队列优先级高于同一轮事件循环中其他microtask队列
修改一下
process.nextTick(() => console.log(1)); console.log(0); setTimeout(()=> { console.log('timer1'); Promise.resolve().then(() => { console.log('promise1'); }); }, 0); process.nextTick(() => console.log(2)); setTimeout(()=> { console.log('timer2'); process.nextTick(() => console.log(3)); Promise.resolve().then(() => { console.log('promise2'); }); }, 0);输出为
与在浏览器中不同,这里promise1并不是在timer1之后输出,因为在setTimeout执行的时候是出于timer阶段,会先一并处理timer回调
setTimeout是优先于setImmediate的,但接下来这个例子却不一定是先执行setTimeout的回调
setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });因为在Node中识别不了0ms的setTimeout,至少也得1ms.
所以,如果在进入该轮事件循环的时候,耗时不到1ms,则setTimeout会被跳过,进入check阶段执行setImmediate回调,先输出 immediate
如果超过1ms,timer阶段中就可以马上处理这个setTimeout回调,先输出 timeout
修改一下代码,读取一个文件让事件循环进入IO文件读取的poll阶段
let fs = require('fs'); fs.readFile('./event.html', () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });这么一来,输出结果肯定就是 先 immediate 后 timeout
五、用好事件循环知道JS的事件循环是怎么样的了,就需要知道怎么才能把它用好
1. 在microtask中不要放置复杂的处理程序,防止阻塞UI的渲染
2. 可以使用process.nextTick处理一些比较紧急的事情
3. 可以在setTimeout回调中处理上轮事件循环中UI渲染的结果
4. 注意不要滥用setInterval和setTimeout,它们并不是可以保证能够按时处理的,setInterval甚至还会出现丢帧的情况,可考虑使用 requestAnimationframe
5. 一些可能会影响到UI的异步操作,可放在promise回调中处理,防止多一轮事件循环导致重复执行UI的渲染
6. 在Node中使用immediate来可能会得到更多的保证
7. 不要纠结
[-_-]眼睛累了吧,注意劳逸结合呀[-_-]
原文出处:https://www.cnblogs.com/imwtr/p/9383695.html



