在以前的一篇文章中简单理了下JS的运行机制,顺着这条线深刻就又遇到了几个概念,什么是事件循环,什么又是宏任务、微任务呢,今天用这篇文章梳理一下。
如下是我本身的理解,若有错误,还望不吝赐教。html
首先你们都知道JS是一门单线程的语言,全部的任务都是在一个线程上完成的。而咱们知道,有一些像I/O,网络请求等等的操做可能会特别耗时,若是程序使用"同步模式"等到任务返回再继续执行,就会使得整个任务的执行特别缓慢,运行过程大部分事件都在等待耗时操做的完成,效率特别低。html5
为了解决这个问题,因而就有了事件循环(Event Loop)这样的概念,简单来讲就是在程序自己运行的主线程会造成一个"执行栈",除此以外,设立一个"任务队列",每当有异步任务完成以后,就会在"任务队列"中放置一个事件,当"执行栈"全部的任务都完成以后,会去"任务队列"中看有没有事件,有的话就放到"执行栈"中执行。node
这个过程会不断重复,这种机制就被称为事件循环(Event Loop)机制。segmentfault
宏任务能够被理解为每次"执行栈"中所执行的代码,而浏览器会在每次宏任务执行结束后,在下一个宏任务执行开始前,对页面进行渲染,而宏任务包括:promise
微任务,能够理解是在当前"执行栈"中的任务执行结束后当即执行的任务。并且早于页面渲染和取任务队列中的任务。宏任务包括:浏览器
他们的运行机制是这样的:网络
在了解了宏任务和微任务以后,整个Event Loop的流程图就能够用下面的流程图来归纳:异步
如无特殊说明,咱们用setTimeout来模拟异步任务,用Promise来模拟微任务。oop
console.log('task start'); setTimeout(()=>{ console.log('setTimeout') },0) new Promise((resolve, reject)=>{ console.log('new Promise') resolve() }).then(()=>{ console.log('Promise.then') }) console.log('task end'); //----------------------执行结果---------------------- // task start // new Promise // task end // Promise.then // setTimeout
这个例子比较简单,就是在主任务上加了一个宏任务(setTimeout),加了一个微任务(Promise.then),看执行的顺序,打印出了主任务的task start、new Promise、task end,主任务完成,接下来执行了微任务的Promise.then,到此第一轮事件循环结束,去任务队列里取出了setTimeout并执行。post
跟上个例子相比,咱们在Promise.then里加上一个setTimeout和一个Promise.then。
console.log('task start'); setTimeout(()=>{ console.log('setTimeout1') },0) new Promise((resolve, reject)=>{ console.log('new Promise1') resolve() }).then(()=>{ console.log('Promise.then1') setTimeout(()=>{ console.log('setTimeout2') },0) new Promise((resolve, reject)=>{ console.log('new Promise2') resolve() }).then(()=>{ console.log('Promise.then2') }) }) console.log('task end'); //----------------------执行结果---------------------- // task start // new Promise1 // task end // Promise.then1 // new Promise2 // Promise.then2 // setTimeout1 // setTimeout2
猜对了么,正常的主任务没有变化,只是在执行第一次微任务的时候,发现了一个宏任务,因而被加进了任务对了。遇到了一个微任务,放到了微任务队列,执行完以后又扫了一遍微任务队列,发现有微任务,因而接着执行完微任务,到这,第一遍事件循环才结束,从任务队列里拿出了两次setTimeout执行了。
其余无异,把刚才添加到Promise.then中的内容添加到setTimeout中。
console.log('task start') setTimeout(()=>{ console.log('setTimeout1') setTimeout(()=>{ console.log('setTimeout2') },0) new Promise((resolve, reject)=>{ console.log('new Promise2') resolve() }).then(()=>{ console.log('Promise.then2') }) },0) new Promise((resolve, reject)=>{ console.log('new Promise1') resolve() }).then(()=>{ console.log('Promise.then1') }) console.log('task end') //----------------------执行结果---------------------- // task start // new Promise1 // task end // Promise.then1 // setTimeout1 // new Promise2 // Promise.then2 // setTimeout2
第一遍主任务执行你们都很明白了,到Promise.then1结束,而后取任务队列中的setTimeout,执行过程当中又发现了一个setTimeout,放到任务队列中,而且发现一个Promise.then2,把这个微任务执行完以后,第二遍事件循环才结束,而后开始第三遍,打印出了setTimeout2。
事件循环遇到事件冒泡会发生什么?
<div class="outer"> <div class="inner"></div> </div>
var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); function onClick() { console.log('click'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('new Promise'); }); } inner.addEventListener('click', onClick); outer.addEventListener('click', onClick);
点击inner,结果:
click //inner的click promise //inner的promise click //outer的click promise //outer的promise timeout //inner的timeout timeout //outer的timeout
我以为解释应该是这样的:
一、开始执行,由于事件冒泡的缘故,事件触发线程会将向上派发事件的任务放入任务队列。接着执行,打印了click,把timeout放入任务队列,把promise放入了微任务队列。
二、执行栈清空,check微任务队列,发现微任务,打印promise,第一遍事件循环结束。
三、从任务队列里取出任务,执行outer的click事件,打印click,把outer的timeout放入任务队列,把outer的promise放入了微任务队列。执行inner放入任务队列的timeout。
四、执行栈清空,check微任务队列,发现微任务,打印promise,第二遍事件循环结束。
五、从任务队列里取出任务,把timeout打印出来。
同样的代码,只不过用JS触发结果就会不同。
对代码作了稍稍改变,将click拆分红两个方法,方便追踪是谁被触发了。
var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); const onInnerClick = (e) => { console.log('inner cilcked'); setTimeout(function() { console.log('inner timeout'); }, 0); Promise.resolve().then(function() { console.log('inner promise'); }); } const onOuterClick = (e) => { console.log('outer clicked'); setTimeout(function() { console.log('outer timeout'); }, 0); Promise.resolve().then(function() { console.log('outer promise'); }); } inner.addEventListener('click', onInnerClick); outer.addEventListener('click', onOuterClick); inner.click();
执行结果:
inner cilcked outer clicked inner promise outer promise inner timeout outer timeout
之因此会出现这样的差别,个人理解是JS代码执行中的click事件,分发了一个同步的冒泡事件。因此在第一个click事件结束以后,调用栈中有outer的click事件,因此出现了两个连续的click。
这也是根据结果猜想过程,内心没底。
加入node环境特有的process.nextTick,再看下面这个例子:
console.log(1); setTimeout(() => { console.log(2); process.nextTick(() => { console.log(3); }); new Promise((resolve) => { console.log(4); resolve(); }).then(() => { console.log(5); }); }); new Promise((resolve) => { console.log(7); resolve(); }).then(() => { console.log(8); }); process.nextTick(() => { console.log(6); }); setTimeout(() => { console.log(9); process.nextTick(() => { console.log(10); }); new Promise((resolve) => { console.log(11); resolve(); }).then(() => { console.log(12); }); });
以上代码会有两个结果
node <11: 1 7 6 8 2 4 9 11 3 10 5 12
node>=11: 1 7 6 8 2 4 3 5 9 11 10 12
NodeJS中微队列主要有2个:
在浏览器中,也能够认为只有一个微队列,全部的microtask都会被加到这一个微队列中,可是在NodeJS中,不一样的microtask会被放置在不一样的微队列中。
Node.js中的EventLoop过程:
**Node 11.x新变化
如今node11在timer阶段的setTimeout,setInterval...和在check阶段的immediate都在node11里面都修改成一旦执行一个阶段里的一个任务就马上执行微任务队列。为了和浏览器更加趋同.**
参考资料:
什么是 Event Loop?
Tasks, microtasks, queues and schedules
js中的宏任务与微任务