workshop, frontend,

Learn concurrency mode in Javascript

Tony Tony Follow Jun 04, 2019 · 5 mins read
Learn concurrency mode in Javascript
Share this

透過async await setTimeout Promise介紹javascript的運行機制,concurrency mode、event loop、同步任務與異步任務.了解函式運行時背後的邏輯,了解背後運作的原理,我們才可以避免不必要的錯誤產生。

Example. 以下代碼的輸出結果為何?

console.log(1);
setTimeout(function () {
    console.log(2);
}, 0);
console.log(3);

Ans. 1 3 2

Example2 以下代碼的輸出結果為何?

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');           
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

Ans.
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

執行過程的分析

  • 遇到 console.log(‘script start’), 放在主線程中立即執行,所以第一步先輸出 script start
  • 遇到 setTimeout,因為 setTimeout 是異步的,所以將回調函數放入異步隊列中去等待,等待主線程中的任務執行完之後,再通過事件輪詢的方式去調用
  • 遇到 async1 函數的調用,此時走到 async1 函數裏面輸出 async1 start
  • 遇到 await 關鍵字,進入到 async2 函數中輸出 async2,將 await 後面的代碼放入微任務中,執行後面的操作
  • 遇到 Promise 直接輸出promise1,將回調函數放入微任務中,執行後面的操作
  • 遇到 console.log(‘script end’), 放在主線程中立即執行,所以第一步先輸出 script end,到這個時候主線程中的代碼就執行完了
  • 主線程中的代碼執行完之後,立即執行所有的微任務。分別輸出 async1 end,promise2
  • 最後通過事件輪詢的方式將異步隊列中代碼,拿到主線程中來執行 (輸出 settimeout)
  • 重覆上個步驟,直到異步隊列清空.

Javascript concurrency mode

單線程(single threaded)

首先,我們要知道 JavaScript 是單線程(single threaded runtime)的程式語言,所有的程式碼片段都會在堆疊中(stack)被執行,而且一次只會執行一個程式碼片段(one thing at a time)。

阻塞(blocking)

當執行程式碼片段需要等待很長一段時間,或好像「卡住」的這種現象,被稱作 阻塞(blocking),假設請求資料的 AJAX Request 變成同步(Synchronous)處理的話,那麼每 request 一次,因為必需等這個函式執行完畢從堆疊(stack)中跳離開後才能往下繼續走,進而導致阻塞的情形產生,以下面的 pseudo code 為例:

// pseudo code
var foo = $.getSync('//foo.com')
var bar = $.getSync('//bar.com')
var qux = $.getSync('//qux.com')

console.log(foo)
console.log(bar)
console.log(qux)

Concurrency and Event Loop

避免在javascripty在執行過程中被耗時的操作所阻塞,javascript使用異步處理諸如事件點擊觸發回調函數、ajax函數、setTimeout等非同步的事件.  

為了要理解 JavaScript 之所以能夠透過非同步的方式(asynchronous)「看起來」一次處理很多事情,我們需要進一步瞭解 Event Loop。 我們之所以可以在瀏覽器中同時(concurrently)處理多個事情,是因為瀏覽器並非只是一個 JavaScript Runtime。

JavaScript 的執行時期(Runtime)一次只能做一件事,但瀏覽器提供了更多不同的 API 讓我們使用,進而讓我們可以透過 event loop 搭配非同步的方式同時處理多個事項。

任務隊列是一個先進先出的隊列,它裏面存放著各種事件和任務。而event loop 的作用是去監控堆疊(call stack)和工作佇列(task queue),當堆疊當中沒有執行項目的時候,便把佇列中的內容拉到堆疊中去執行。

所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。

同步任務

同步任務:在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務。  

  • 輸出 如:console.log()
  • 變量的聲明
  • 同步函數:如果在函數返回的時候,調用者就能夠拿到預期的返回值或者看到預期的效果,那麽這個函數就是同步的。

異步任務

  • setTimeout和setInterval
  • DOM事件
  • Promise
  • process.nextTick
  • fs.readFile
  • http.get
  • 異步函數:如果在函數返回的時候,調用者還不能夠得到預期結果,而是需要在將來通過一定的手段得到,那麽這個函數就是異步的。

除此之外,任務隊列又分為macro-task(宏任務)與micro-task(微任務),在ES5標準中,它們被分別稱為task與job。

宏任務

  • I/O
  • setTimeout
  • setInterval
  • setImmdiate
  • requestAnimationFrame

微任務

  • process.nextTick
  • Promise
  • Promise.then
  • MutationObserver

宏任務和微任務的執行順序

一次事件循環中,先執行宏任務隊列裏的一個任務,再把微任務隊列裏的所有任務執行完畢,再去宏任務隊列取下一個宏任務執行。

註:在當前的微任務沒有執行完成時,是不會執行下一個宏任務的。

總結

JavaScript通過事件循環和瀏覽器各線程協調共同實現異步。同步可以保證順序一致,但是容易導致阻塞;異步可以解決阻塞問題,但是會改變順序性。

  • 理解JS的單線程的概念:一段時間內做一件事
  • 理解任務隊列:同步任務、異步任務
  • 理解 Event Loop
  • 理解哪些語句會放入異步任務隊列
  • 理解語句放入異步任務隊列的時機

更詳盡的內容可以看 Philip Roberts 在 JS Conf 的演講影片 What the heck is the event loop anyway? 和 MDN Concurrency model and Event Loop

有興趣的可以試著做以下兩道練習題  

async function async1() {
    console.log('async1 start');
    await async2();
    setTimeout(function() {
        console.log('setTimeout1')
    },0)
}
async function async2() {
	setTimeout(function() {
		console.log('setTimeout2')
	},0)
}
console.log('script start');

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

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

async function async1() {
   console.log('async1 start')
   await async2()
   console.log('async1 end')
}
async function async2() {
   console.log('async2')
	new Promise((resolve, reject) => {
		console.log('promise3')
		resolve()
	})
	.then(body => {
		console.log('promise4')
	})
}
console.log('script start')
setTimeout(function () {
   console.log('settimeout')
})
async1()
new Promise(function (resolve) {
   console.log('promise1')
   resolve()
}).then(function () {
   console.log('promise2')
})
console.log('script end')

Ans. 1

    script start
    async1 start
    promise1
    script end
    promise2
    setTimeout3
    setTimeout2
    setTimeout1

2

    script start
    async1 start
    async2
    promise3
    promise1
    script end
    promise4
    async1 end
    promise2
    settimeout
Join Newsletter
Get the latest news right in your inbox. We never spam!
Tony
Written by Tony Follow
Hi, I am Tony, the author of Learning Journey blog. I hope you like what I sharing!