大話javascript 4期:事件循環(2)

1、任務隊列

同步任務與異步任務的由來

單線程就意味着,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就不得不一直等着。前端

若是排隊是由於計算量大,CPU忙不過來,倒也算了,可是不少時候CPU是閒着的,由於IO設備(輸入輸出設備)很慢(好比Ajax操做從網絡讀取數據),不得不等着結果出來,再往下執行。node

JavaScript語言的設計者意識到,這時主線程徹底能夠無論IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備返回告終果,再回過頭,把掛起的任務繼續執行下去。ajax

因而,全部任務能夠分紅兩種,一種是同步任務(synchronous),另外一種是異步任務(asynchronous)vim

同步任務與異步任務的定義

同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;瀏覽器

異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。網絡

異步執行的運行機制

具體來講,異步執行的運行機制以下。(同步執行也是如此,由於它能夠被視爲沒有異步任務的異步執行。)數據結構

clipboard.png

(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。異步

(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。async

(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。函數

(4)主線程不斷重複上面的第三步。

只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。這個過程會不斷重複。

舉個例子:

console.log("1");
setTimeout(()=>{
    console.log("2");
},0);
console.log("3");
// 1
// 3
// 2

運行結果是:一、三、2
setTimeout裏的函數並無當即執行,而是延遲一段時間,符合特定的條件纔開始執行,這就是異步執行操做

console.log("1")  //是同步任務,放入主線程,
setTimeout()      //是異步任務,被放入事件列表Event table中,0秒後被推入任務隊列task queue裏,
console.log("3")  //是同步任務,放入主線程
//當一、3任務先執行完後,主線程去task queue(事件隊列)裏查看是否有可執行的函數,執行setTimeout裏的函數。

2、事件和回調函數

"任務隊列"是一個事件的隊列(也能夠理解成消息的隊列),IO設備完成一項任務,就在"任務隊列"中添加一個事件,表示相關的異步任務能夠進入"執行棧"了。主線程讀取"任務隊列",就是讀取裏面有哪些事件。

"任務隊列"中的事件,除了IO設備的事件之外,還包括一些用戶產生的事件(好比鼠標點擊、頁面滾動等等)。只要指定過回調函數,這些事件發生時就會進入"任務隊列",等待主線程讀取。

所謂"回調函數"(callback),就是那些會被主線程掛起來的代碼。異步任務必須指定回調函數當主線程開始執行異步任務,就是執行對應的回調函數。

"任務隊列"是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,"任務隊列"上第一位的事件就自動進入主線程。可是,因爲存在後文提到的"定時器"功能,主線程首先要檢查一下執行時間,某些事件只有到了規定的時間,才能返回主線程。

clipboard.png

3、Event Loop

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

clipboard.png

  1. 主線程運行的時候,產生(heap)和(stack),
    heap(堆):是用戶主動請求而劃分出來的內存區域,好比你new Object(),就是將一個對象存入堆中,能夠理解爲heap存對象。
    stack(棧):是因爲函數運行而臨時佔用的內存區域,函數都存放在棧裏。
  2. 棧中的代碼調用各類外部API,它們在"任務隊列"中加入各類事件(click,load,done)。
    (當知足觸發條件後才加入隊列,如ajax請求完畢)
  3. 而當棧中的代碼執行完畢,主線程就會去讀取"任務隊列",依次執行那些事件所對應的回調函數。如此循環

【注意,老是要等待棧中的代碼執行完畢後纔會去讀取事件隊列中的事件】

4、宏任務和微任務

JS中分爲兩種任務類型: 宏任務macro task微任務micro task
在ECMAScript中,micro task稱爲jobs,macro task可稱爲task

宏任務與微任務的定義

1)宏任務(macro task),能夠理解爲每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)
每個task會從頭至尾將這個任務執行完畢,不會執行其它

瀏覽器爲了可以使得JS內部task與DOM任務可以有序的執行,會在一個task執行結束後,在下一個 task 執行開始前,對頁面進行從新渲染
task->渲染->task->...

2)微任務(micro task),能夠理解爲在當前 task 執行結束後當即執行的任務
也就是說,在當前task任務後,下一個task以前,在渲染以前

因此它的響應速度相比setTimeout(setTimeout是task)會更快,由於無需等渲染
也就是說,在某一個macro task執行完後,就會將在它執行期間產生的全部micro task都執行完畢(在渲染前)

常見的宏任務和微任務:

1)宏任務(macro task):
主代碼塊setTimeoutsetInterval,I/O、UI交互事件、postMessage、MessageChannel、setImmediate(node.js 環境)等(能夠看到,事件隊列中的每個事件都是一個宏任務)

2)微任務(micro task):
Promise.thenMutaionObserverMessageChannelprocess.nextTick(node.js 環境)等

__補充:在node環境下,process.nextTick的優先級高於Promise,也就是能夠簡單理解爲:在宏任務結束後會先執行微任務隊列中的nextTickQueue部分,而後纔會執行微任務中的Promise部分。

再根據線程來理解下:
宏任務(macro task)中的事件都是放在一個事件隊列中的,而這個隊列由事件觸發線程維護
微任務(micro task)中的全部微任務都是添加到微任務隊列(Job Queues)中,等待當前宏任務執行完畢後執行,而這個隊列由JS引擎線程維護

因此,總結下運行機制

  1. 執行一個宏任務(棧中沒有就從事件隊列中獲取)
  2. 執行過程當中若是遇到微任務,就將它添加到微任務的任務隊列中
  3. 宏任務執行完畢後,當即執行當前微任務隊列中的全部微任務(依次執行)
  4. 當前宏任務執行完畢,開始檢查渲染,而後GUI線程接管渲染
  5. 渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)

clipboard.png

clipboard.png

clipboard.png

舉個例子:

setTimeout(()=>{
console.log("定時器開始執行");
})
 
new Promise(function(resolve){
    console.log("準備執行for循環了");
    for(var i=0;i<100;i++){
        i==22&&resolve();
    }
}).then(()=>console.log("執行then函數"));
 
console.log("代碼執行完畢");


//首先執行script下的宏任務,遇到setTimeout,將其放到宏任務的【隊列】裏

//遇到 new Promise直接執行,打印"準備執行for循環" 

//遇到then方法,是微任務,將其放到微任務的【隊列裏】
 
//打印 "代碼執行完畢" 

//本輪宏任務執行完畢,查看本輪的微任務,發現有一個then方法裏的函數, 打印"執行then函數" 

//到此,本輪的event loop 所有完成。 

//下一輪的循環裏,先執行一個宏任務,發現宏任務的【隊列】裏有一個 setTimeout裏的函數,執行打印"定時器開始執行"

因此最後的執行順序就是:【準備執行for循環-->代碼執行完畢-->執行then函數-->定時器開始執行】

clipboard.png

若是你以爲這篇文章對你有所幫助,那就順便點個贊吧,點點關注不迷路~

黑芝麻哇,白芝麻發,黑芝麻白芝麻哇發哈!

前端哇發哈

相關文章
相關標籤/搜索