在以前的一篇文章中簡單理了下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中的宏任務與微任務