帶你瞭解JavaScript的運行機制—Event Loop

JS 是單線程的。

首先,衆所周知,JS 是單線程的,爲何這種低效的運行方式依舊沒有被淘汰那?這是由它的用途決定的;JS 主要用途是用戶交互和DOM操做,舉例來講假如js同時有兩線程,一個線程在某個DOM節點上添加內容,另外一個線程卻刪除了這個節點,這時候瀏覽器就不知所措了,該以哪一個線程爲標準那?(爲了提升運行性能,新的 html5 裏添加了web worker,其能在主線程內添加子線程,可是限制了其沒法操做DOM。)html

任務隊列(task queue)

因爲 JS 是單線程,因此任務的執行就須要排隊,一個一個執行,前一個任務結束了,下一個任務才能開始。可是當一個任務是異步任務時,瀏覽器就須要等待較長時間,才能獲得它的返回結果繼續執行,中間等待的時間cpu是空閒。JS 的應對方案是,將該任務暫時擱置,去執行其餘任務。當有返回結果時再從新回來執行該任務。前端

這個暫時擱置,擱置於何處那,答案就是任務隊列html5

同步任務是指在主線程上執行的任務,只有前一個任務執行完畢,下一個任務才能執行。 異步任務是指不進入主線程,而是進入任務隊列(task queue)的任務,只有主線程任務執行完畢,任務隊列的任務纔會進入主線程執行。web

執行棧(JS stack)

首先,咱們先來了解一下堆(heap)和棧(stack)的概念。棧是用來靜態分配內存的而堆是動態分配內存的,它們都是存在於計算機內存之中。堆是先進後出,棧(堆棧)是先進先出的。js的全部任務都是在js執行棧中執行的。先進入棧的任務後執行,可是大部分時候js執行棧內都只有一個任務。(下文會說起)chrome

宏任務和微任務(task & Microtask)

上文說道異步任務不在主線程上執行,其實不僅僅是異步任務,全部的微任務都不在主線程上執行。由此其實咱們能夠將上文的任務隊列稱之爲微任務隊列。宏任務直接在主線程上自行,而微任務須要進入爲任務隊列,等待執行。promise

咱們看一下代碼(example1)瀏覽器

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');
複製代碼

這個輸出結果是什麼那?bash

順序是:微信

script start
script end
promise1
promise2
setTimeout
複製代碼

首先咱們視整段代碼爲一個 script 標籤,它做爲一個宏任務,直接進入js執行棧中執行:dom

輸出==script start==;

遇到setTimeout,而0秒後setTimeout做爲一個獨立的宏任務加入到"宏任務隊列"中。(注意這裏說的是宏任務隊列,也就是上文所說的主線程);

遇到promise,promise完成後的第一個then做爲一個獨立的微任務加入到「微任務隊列」中,第二個then又作爲一個微任務加入到微任務的隊列中。

而後輸出==script end==;

如今,咱們來理一下:script一整個宏任務執行完畢了,這時候js執行棧是空的,宏任務隊列(主線程)中有一個setTimeout,而微任務隊列中有兩個promise(then)任務。先執行哪一個?回想咱們以前說的異步任務執行策略,就不難推測,下一個進入js執行棧就是第一個promise(then);

輸出 ==promise1==;

而後此時再看宏任務隊列和微任務隊列。微任務隊列還有一個promise(then),因此將這個微任務壓入js執行棧執行;

輸出==promise2==;

此時,微任務隊列爲空,因此再去執行宏任務隊列中的任務,setTimeout;

輸出==setTimeout==;

總結來講,任務分爲宏任務和微任務,對應宏任務隊列(主線程)和微任務隊列。微任務是在當前正在執行腳本結束以後當即執行的任務。當一個任務執行結束後,JS 執行棧空出來,這時候會首先去微任務隊列中尋找任務,當微任務隊列不爲空時,將一個微任務加入到 JS 執行棧中。噹噹前的微任務隊列爲空時,再去執行宏任務隊列中的任務。

如何區分微任務和宏任務:

宏任務(task):是嚴格按照時間順序壓棧和執行的,因此瀏覽器可以使得 JavaScript 內部任務與 DOM 任務可以有序的執行。當一個 task 執行結束後,在下一個 task 執行開始前,瀏覽器能夠對頁面進行從新渲染。每個 task 都是須要分配的,例如從用戶的點擊操做到一個點擊事件,渲染HTML文檔,同時還有上面例子中的 setTimeout。

setTimeout 的工做原理相信你們應該都知道,其中的延遲並非徹底精確的,這是由於 setTimeout 它會在延遲時間結束後分配一個新的 task 至 event loop 中,而不是當即執行,因此 setTimeout 的回調函數會等待前面的 task 都執行結束後再運行。這就是爲何 'setTimeout' 會輸出在 'script end' 以後,由於 'script end' 是第一個 task 的其中一部分,而 'setTimeout' 則是一個新的 task。

微任務(Microtask):一般來講就是須要在當前 task 執行結束後當即執行的任務,例如須要對一系列的任務作出迴應,或者是須要異步的執行任務而又不須要分配一個新的 task,這樣即可以減少一點性能的開銷。microtask 任務隊列是一個與 task 任務隊列相互獨立的隊列,microtask 任務將會在每個 task 任務執行結束以後執行。每個 task 中產生的 microtask 都將會添加到 microtask 隊列中,microtask 中產生的 microtask 將會添加至當前隊列的尾部,而且 microtask 會按序的處理完隊列中的全部任務。microtask 類型的任務目前包括了 MutationObserver 以及 Promise 的回調函數。

每當一個 Promise 被決議(或是被拒絕),便會將其回調函數添加至 microtask 任務隊列中做爲一個新的 microtask 。這也保證了 Promise 能夠異步的執行。因此當咱們調用 .then(resolve, reject) 的時候,會當即生成一個新的 microtask 添加至隊列中,這就是爲何上面的 'promise1' 和 'promise2' 會輸出在 'script end' 以後,由於 microtask 任務隊列中的任務必須等待當前 task 執行結束後再執行,而 'promise1' 和 'promise2' 輸出在 'setTimeout' 以前,這是由於 'setTimeout' 是一個新的 task,而 microtask 執行在當前 task 結束以後,下一個 task 開始以前。


進階版,帶你深刻task & Microtask(example2):

<body>
    <div class="outer">
      <div class="inner"></div>
    </div>
</body>
<script>
    var outer = document.querySelector('.outer');
    var inner = document.querySelector('.inner');

    new MutationObserver(function() {
      console.log('mutate');
    }).observe(outer, {
      attributes: true
    });

    function onClick() {
      console.log('click');
    
      setTimeout(function() {
        console.log('timeout');
      }, 0);

      Promise.resolve().then(function() {
        console.log('promise');
      });

      outer.setAttribute('data-random', Math.random());
    }

    inner.addEventListener('click', onClick);
    outer.addEventListener('click', onClick);
</script>
複製代碼

當咱們點擊inner這個div的時候會輸出什麼那?

順序是:

click
promise
mutate
click
promise
mutate
timeout
timeout
複製代碼

爲什麼是如此那?

這裏要說明的是一個click操做做爲一個宏任務,當這個inner的click對應的監聽函數執行完後,即視爲一個任務的完成,此時執行微任務隊列中的promise(then)和 mutationObserver的回調。這兩個任務執行完成後微任務隊列爲空,而後再執行冒泡形成的outter的click。當outter的click任務和微任務都執行完後,纔會再去找宏任務隊列(主線程)中剩下的兩個setTimeout的任務。並將其一個一個的壓入執行棧。


超級進階版(example3):

當咱們在上面的js代碼中加入下面這行代碼時,會有什麼不一樣嗎?

inner.click()
複製代碼

答案是:

click
click
promise
mutate
promise
timeout
timeout
複製代碼

爲什麼會有如此大的不一樣那?下面咱們來仔細分析:

上一個例子中兩個微任務在兩個click之間執行,而這個例子中,倒是在兩個click以後執行的;

首先inner.click()觸發的事件做爲一個任務壓入執行棧,由此產生的inner的監聽函數函數又作爲一個任務壓入執行棧,當這個回調函數產生的任務執行完畢後,輸出了 click,且微任務隊列裏面增長promise和mutate,那按上面的說法不是應該執行promise和mutate嗎?然而並非,由於此時 JS 執行棧內的inner.click()尚未執行結束,因此繼續inner.click()的事件觸發outter的監聽函數,由此再輸出click,該回調結束後,inner.click()這個任務纔算是結束,此時纔會去執行微任務隊列中的任務。

簡單來講,在這個例子中,因爲咱們調用 inner.click() ,使得事件監聽器的回調函數和當前運行的腳本同步執行而不是異步,因此當前腳本的執行棧會一直壓在 JS 執行棧 當中。因此在這個例子中的微任務不會在每個 click 事件以後執行,而是在兩個 click 事件執行完成以後執行。

Event Loop

JS 執行棧不斷的從主線程中和微任務隊列讀取任務並執行,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)

:本文全部運行結果皆給予chrome瀏覽器,其餘瀏覽器或有出入

參考文章jakearchibald.com/2015/tasks-…

做者簡介

琦玉,銅板街前端開發工程師,2018年1月加入團隊,目前主要負責大數據團隊前端項目開發。

更多精彩內容,請掃碼關注 「銅板街科技」 微信公衆號。
相關文章
相關標籤/搜索