iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

1
Modern Web

《透過認知心理學認識 JavaScript》貓咪也能輕鬆學習的 JavaScript系列 第 32

【修正模型】4-3 事件循環(Event Loop)與任務隊列(Job Queue)

同步(Synchronous)與非同步(Asynchronous)

在理解執行上下文與呼叫堆疊之後,眼尖的讀者應該會發現一個問題,那就是既然程式碼是幾乎是一行一行解讀的情況下,那假如我在資源請求(request)的時候載入一個非常大的檔案,那麼我的程式不就停在這裡了嗎?

function getPageData() {
  var result;
  fetch('https://raw.githubusercontent.com/shawnlin0201/ithelp-2020/main/3-4-1-fetch-request.json')
    .then(res => res.json())
    .then(res => {
        result = res
    })
   
  return result
}

var data = getPageData()

以上方程式碼為例,假設我在程式碼中執行了這個 getPageData 函式,並透過 fetch 向某個伺服器請求資源,但如果依照先前解析的概念來說,這時我們得等待最後資源請求的內容回來之後,接著才會離開 getPageData 當下所創造出來的執行上下文中,並把結果交還給 data

然而實際上執行的時候你會發現無論你再怎麼請求,data 內永遠記錄的是 undefined

而當你想透過 console.log 來確認資料是否會回傳時,卻發現資料是有回來的:

function getPageData() {
  var result;
  fetch('https://raw.githubusercontent.com/shawnlin0201/ithelp-2020/main/3-4-1-fetch-request.json')
    .then(res => res.json())
    .then(res => {
        console.log('res', res) // 有資料
        result = res
    })
   
  return result
}

var data = getPageData() // 仍然是 undefined

這時你可能會開始懷疑之前所理解的那套認知歷程不太對勁,實際上這是因為在 JavaScript 中存在著 非同步 的作法在裡面。

而同步與非同步的概念所指的是在執行程式碼的時候主要分為兩種執行結果:會 立即返回資料 的即是同步的程式碼;相對的,不會立即返回資料 的即是非同步的程式碼。

並且其中最重要的概念是 立即與不立即的區別並不是以秒數來決定的,而是這些程式運用到了瀏覽器中的 WEB APIs 的一些相關機制:

  • 計時器(Timer):setTimeoutsetInterval
  • 資源請求、等待回應類:XMLHttpRequestfetchpromise
  • 使用者操作類:鍵盤事件、滑鼠點擊事件等

所以即便你使用計時器輸入等待 0 秒時,該段程式碼仍然被視為需要被非同步所執行的程式碼:

setTimeout(function(){
  
  // 在這個區塊程式碼中所執行的程式會採用非同步的方法執行

},'0') // 等待零秒

同步與非同步的執行

基本上在瀏覽器中所有的程式碼只會在 主線程中執行,而一般執行的過程就好比我們在執行上下文中(Execution Context)與呼叫堆疊(Call Stack)的章節時所講的一樣。

然而被非同步執行的程式碼,被引擎解析到的當下,會先排入在瀏覽器中各自屬於的隊伍(如 setTimeout 會有個 watcher 的隊伍):

接著達到條件(例如計時的時間到或是請求的資源回應時),就會被排入一個叫做任務隊列(Job Queue)或稱事件隊列(Event Queue)的隊伍當中。

最後當整個主程式的程式碼都執行完畢時,這時才會開始從任務隊列中陸續的將程式碼放回主線程當中,接著依照主線程執行程式碼的方式繼續執行,等到下次主線程又沒有程式碼時,就會繼續輪詢各個事件處理器與任務隊列是否還有東西要執行。

若你對於整個過程的視覺化處理有興趣的話,可以參考 這個專案 所做的內容。

延伸閱讀:Microtasks & Marcotask

上方所介紹的任務隊列(Job Queue)實際上還可以分為 MarcotaskMircotask 兩個隊伍,而各自所排入的事件分別為:

  • Marcotasks:setTimeoutsetInterval、使用者操作、UI 渲染
  • Mircotasks:PromisesMutationObserver

並且在執行順序上 Mircotasks 優於 Marcotasks 中的隊伍,且會等到 Mircotasks 都執行完了才會回來檢查 Marcotasks

console.log('start')

setTimeout(() => {
  console.log('setTimeout1')
})
Promise.resolve()
  .then(()=> {
    console.log('Promise1')
  })
  .then(()=> {
    console.log('Promise2')
  })
setTimeout(() => {
  console.log('setTimeout2')
})
console.log('end')

執行完畢會顯示:

'start'
'end'
'Promise1'
'Promise2'
'setTimeout1'
'setTimeout2'

非同步的處理方式

現在我們除了會分辨哪些程式碼是被非同步的執行後,現在我們要回頭來解決上面一開始所遇到的問題。

function getPageData() {
  var result;
  fetch('https://raw.githubusercontent.com/shawnlin0201/ithelp-2020/main/3-4-1-fetch-request.json')
    .then(res => res.json())
    .then(res => {
        console.log('res', res) // 有資料
        result = res
    })
   
  return result
}

var data = getPageData() // 仍然是 undefined

既然我們現在知道非同步的程式碼會排入另一個隊列中再回來執行,因此我們可以藉由要把後續做的事情放在需要被非同步執行的程式碼中,最後即可跟著那些程式碼一起被執行:

function getPageData() {
  fetch('...')
    .then(res => res.json())
    .then(res => {
      // 在這裡做後續的事情
    })
}

但是這樣做不僅會讓該函式做的事情被限縮,其他人也要使用這個函式取得資源,就會被迫也要跟著處理後續的問題,因此較好的作法可以藉由回呼函式將內容拋出:

function getPageData(callback) {
  fetch('...')
    .then(res => res.json())
    .then(res => {
      callback(res)
    })
}

如此一來我們只要有使用 getPageData 的需求,就可以透過傳入函式來決定我們取到資料後要做什麼事情:

getPageData(function(res){
  console.log('Get Responses', res)
  // do something
})

當然若你想使用更新的語法如 Async/Await 也同樣可以處理,因為非同步的機制還是一致的。而到了這裡其實你已經將 JavaScript 大致上的概念都走了一輪,剩下的部分基本上就可以依照實作中的需求去搜尋,必要時再去做深入的瞭解與使用,明天我們將回過頭來檢視整個過去所學到的內容。


上一篇
【修正模型】4-2 呼叫堆疊(Call Stack)
下一篇
【修正模型】4-4 完賽,但我們才正要開始
系列文
《透過認知心理學認識 JavaScript》貓咪也能輕鬆學習的 JavaScript33

尚未有邦友留言

立即登入留言