JS的代码执行是基于一种事件循环的机制,之因此称做事件循环,MDN给出的解释为css
由于它常常被用于相似以下的方式来实现html
while (queue.waitForMessage()) { queue.processNextMessage(); }若是当前没有任何消息
queue.waitForMessage
会等待同步消息到达html5
咱们能够把它当成一种程序结构的模型,处理的方案。更详细的描述能够查看 这篇文章node
而JS的运行环境主要有两个:浏览器、Node。git
在两个环境下的Event Loop实现是不同的,在浏览器中基于 规范 来实现,不一样浏览器可能有小小区别。在Node中基于 libuv 这个库来实现github
JS是单线程执行的,而基于事件循环模型,造成了基本没有阻塞(除了alert或同步XHR等操做)的状态web
根据 规范,每一个线程都有一个事件循环(Event Loop),在浏览器中除了主要的页面执行线程 外,Web worker是在一个新的线程中运行的,因此能够将其独立看待。segmentfault
每一个事件循环有至少一个任务队列(Task Queue,也能够称做Macrotask宏任务),各个任务队列中放置着不一样来源(或者不一样分类)的任务,可让浏览器根据本身的实现来进行优先级排序api
以及一个微任务队列(Microtask Queue),主要用于处理一些状态的改变,UI渲染工做以前的一些必要操做(能够防止屡次无心义的UI渲染)promise
主线程的代码执行时,会将执行程序置入执行栈(Stack)中,执行完毕后出栈,另外有个堆空间(Heap),主要用于存储对象及一些非结构化的数据
一开始
宏任务与微任务队列里的任务随着:任务进栈、出栈、任务出队、进队之间交替着进行
从macrotask队列中取出一个任务处理,处理完成以后(此时执行栈应该是空的),从microtask队列中一个个按顺序取出全部任务进行处理,处理完成以后进入UI渲染后续工做
须要注意的是:microtask并非在macrotask完成以后才会触发,在回调函数以后,只要执行栈是空的,就会执行microtask。也就是说,macrotask执行期间,执行栈多是空的(好比在冒泡事件的处理时)
而后循环继续
常见的macrotask有:
run <script>(同步的代码执行)
setInterval
setImmediate (Node环境中)
requestAnimationFrame
I/O
UI rendering
常见的microtask有:
process.nextTick (Node环境中)
Promise callback
Object.observe (基本上已经废弃)
MutationObserver
macrotask种类不少,还有 dispatch event事件派发等
run <script>这个可能看起来比较奇怪,能够把它当作一段代码(针对单个<script>标签)的同步顺序执行,主要用来描述执行程序的第一步执行
dispatch event主要用来描述事件触发以后的执行任务,好比用户点击一个按钮,触发的onClick回调函数。须要注意的是,事件的触发是同步的,这在下文有例子说明
注:
固然,也可认为 run <script>不属于macrotask,毕竟规范也没有这样的说明,也能够将其视为主线程上的同步任务,不在主线程上的其余部分为异步任务
先来看看这段蛮复杂的代码,思考一下会输出什么
console.log('start'); var intervalA = setInterval(() => { console.log('intervalA'); }, 0); setTimeout(() => { console.log('timeout'); clearInterval(intervalA); }, 0); var intervalB = setInterval(() => { console.log('intervalB'); }, 0); var intervalC = setInterval(() => { console.log('intervalC'); }, 0); new Promise((resolve, reject) => { console.log('promise'); for (var i = 0; i < 10000; ++i) { i === 9999 && resolve(); } console.log('promise after for-loop'); }).then(() => { console.log('promise1'); }).then(() => { console.log('promise2'); clearInterval(intervalB); }); new Promise((resolve, reject) => { setTimeout(() => { console.log('promise in timeout'); resolve(); }); console.log('promise after timeout'); }).then(() => { console.log('promise4'); }).then(() => { console.log('promise5'); clearInterval(intervalC); }); Promise.resolve().then(() => { console.log('promise3'); }); console.log('end');
上述代码结合了常规执行代码,setTimeout,setInterval,Promise
答案为
在解释为何以前,先看一个更简单的例子
console.log('start'); setTimeout(() => { console.log('timeout'); }, 0); Promise.resolve().then(() => { console.log('promise'); }); console.log('end');
大概的步骤,文字有点多
1. 运行时(runtime)识别到log方法为通常的函数方法,将其入栈,而后执行输出 start 再出栈
2. 识别到setTimeout为特殊的异步方法(macrotask),将其交由其余内核模块处理,setTimeout的匿名回调函数被放入macrotask队列中,并设置了一个 0ms的当即执行标识(提供后续模块的检查)
3. 识别到Promise的resolve方法为通常的方法,将其入栈,而后执行 再出栈
4. 识别到then为Promise的异步方法(microtask),将其交由其余内核模块处理,匿名回调函数被放入microtask队列中
5. 识别到log方法为通常的函数方法,将其入栈,而后执行输出 end 再出栈
6. 主线程执行完毕,栈为空,随即从microtask队列中取出队首的项,
这里队首为匿名函数,匿名函数里面有 console的log方法,也将其入栈(若是执行过程当中识别到特殊的方法,就在这时交给其余模块处理到对应队列尾部),
输出 promise后出栈,并将这一项从队列中移除
7. 继续检查microtask队列,当前队列为空,则将当前macrotask出队,进入下一步(若是不为空,就继续取下一个microtask执行)
8.检查是否须要进行UI从新渲染等,进行渲染...
9. 进入下一轮事件循环,检查macrotask队列,取出一项进行处理
因此最终的结果是
再看上面那个例子,对比起来只是代码多了点,混入了setInterval,多个setTimeout与promise的函数部分,按照上面的思路,应该不难理解
须要注意的三点:
1. clearInterval(intervalA); 运行的时候,实际上已经执行了 intervalA 的macrotask了
2. promise函数内部是同步处理的,不会放到队列中,放入队列中的是它的then或catch回调
3. promise的then返回的仍是promise,因此在输出promise4后,继续检测到后续的then方法,立刻放到microtask队列尾部,再继续取出执行,立刻输出promise5;
而输出promise1以后,为何没有立刻输出promise2呢?由于此时promise1所在任务以后是promise3的任务,1和3在promise函数内部返回后就添加至队列中,2在1执行以后才添加
再来看个例子,就有点微妙了
<script> console.log('start'); setTimeout(() => { console.log('timeout1'); }, 0); Promise.resolve().then(() => { console.log('promise1'); }); </script> <script> setTimeout(() => { console.log('timeout2'); }, 0); requestAnimationFrame(() => { console.log('requestAnimationFrame'); }); Promise.resolve().then(() => { console.log('promise2'); }); console.log('end'); </script>
输出结果
requestAnimationFrame是在setTimeout以前执行的,start以后并非直接输出end,也许这两个<script>标签被独立处理了
来看一个关于DOM操做的例子,Tasks, microtasks, queues and schedules
<style type="text/css"> .outer { width: 100px; background: #eee; height: 100px; margin-left: 300px; margin-top: 150px; display: flex; align-items: center; justify-content: center; } .inner { width: 50px; height: 50px; background: #ddd; } </style> <script> var outer = document.querySelector('.outer'), inner = document.querySelector('.inner'), clickTimes = 0; new MutationObserver(() => { console.log('mutate'); }).observe(outer, { attributes: true }); function onClick() { console.log('click'); setTimeout(() => { console.log('timeout'); }, 0); Promise.resolve().then(() => { console.log('promise'); }); outer.setAttribute('data-click', clickTimes++); } inner.addEventListener('click', onClick); outer.addEventListener('click', onClick); // inner.click(); // console.log('done'); </script>
点击内部的inner块,会输出什么呢?
MutationObserver优先级比promise高,虽然在一开始就被定义,但其实是触发以后才会被添加到microtask队列中,因此先输出了promise
两个timeout回调都在最后才触发,由于click事件冒泡了,事件派发这个macrotask任务包括了先后两个onClick回调,两个回调函数都执行完以后,才会执行接下来的 setTimeout任务
期间第一个onClick回调完成后执行栈为空,就立刻接着执行microtask队列中的任务
若是把代码的注释去掉,使用代码自动 click(),思考一下,会输出什么?
能够看到,事件处理是同步的,done在连续输出两个click以后才输出
而mutate只有一个,是由于当前执行第二个onClick回调的时候,microtask队列中已经有一个MutationObserver,它是第一个回调的,由于事件同步的缘由没有被及时执行。浏览器会对MutationObserver进行优化,不会重复添加监听回调
在Node环境中,macrotask部分主要多了setImmediate,microtask部分主要多了process.nextTick,而这个nextTick是独立出来自成队列的,优先级高于其余microtask
不过事件循环的的实现就不太同样了,能够参考 Node事件文档 libuv事件文档
Node中的事件循环有6个阶段
setTimeout()
和 setInterval()
中到期的callbacksocket.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. 不要纠结