iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 5
5
Modern Web

從0.5開始的JavaScript系列 第 5

Day5 單執行緒&非同步發生的血案

在學習 JS 時,必須要知道它是單執行緒(單線程),最好理解的方法就是它一次只能做一件事。

那這就怪了,同步執行下,setTimeoutevent callbackHttp request 這些耗時、不確定觸發執行時間的操作,不就會使程式阻塞,都在等它們就飽了?

所幸可以透過同步/非同步來處理這些事件,將這些耗時、無法預期執行時間的操作以非同步處理,等執行完同步的程式碼再來處理它們。

真是可喜可賀可喜可賀~

Hmm...不對吧,你不是說 JS 是同步執行,哪來的非同步?
抓到了,你再胡說我要退訂了!

等等,讓我解釋清楚,不要讓已經夠少的訂閱歸零Q_Q

瀏覽器內核

先記得一個重點: JS 的引擎是單執行緒的,瀏覽器無論在什麼時候都只有一個線程在執行 JS 程式。

這邊引用知乎上面的問答(來源找不到,歡迎幫忙補上),並盡量修改成台灣的用語,

瀏覽器的內核是多線程的,它們在內核控制下相互配合以保持同步,至少實現三個常駐線程: 1. JavaScript 引擎線程、2. GUI 渲染線程、3. 瀏覽器事件觸發線程。

3.事件觸發線程: 當一個事件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待 JavaScript 引擎的處理。
這些事件可來自 JavaScript 引擎當前執行的程式碼,如 setTimeout、也可來自瀏覽器內核的其他線程如滑鼠點擊、Ajax 異步請求等,但由於 JavaScript 的單線程關係,所有這些事件都得排隊等待 JavaScript 引擎處理(當線程中沒有執行任何同步程式碼的前提下才會執行異步程式碼)。

看到上面的描述,我們統整出兩個重點,

  1. 瀏覽器內核除了 js 引擎的執行緒,還有其他同步執行的執行緒。
  2. setTimeouteventajax,或其他無法預期執行時間的操作,都會以非同步處理。也就是會先被丟到事件佇列(Queue),等到同步執行的程式碼執行完,才會去處理那些被放到佇列中的任務。

結合昨天講的,執行 function 時,會依執行順序把 function 丟到 主執行緒(stack) 中,等到 stack 中的任務都執行完畢,才會將 事件佇列(Queue) 中待執行的任務拉到 主執行緒(stack) 中執行,執行完畢(stack清空)後,再到 事件佇列(Queue) 中查看是否還有待執行任務,這個查看的過程就稱為 Event Loop

講了那麼多,先直接來看個範例吧,
請問印出的順序為何?

setTimeout(function(){console.log("1sec")}, 1000);
console.log("Hi");

Ans:
Hi => 1sec

這看起來可能沒什麼問題,但這個呢,
請問印出的順序為何?

setTimeout(function(){console.log("0sec")}, 0);
console.log("Hi");

沒錯,答案還是
Hi => 0sec

在還未了解到今天講的觀念時,可能會黑人問號,0sec 不是會馬上執行嗎,怎麼還是先印出 Hi ?

好吧,這個問題不太好,因為 HTML5 規定了 setTimeout 的等待時間小於 4ms 時會自動補足。

那我們把問題改成這樣,要等到 3 秒後才會印出 Hi,這怎麼想 0sec 都會先印出吧?

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

var now = Date.now();
while(true){
  if(Date.now() > now + 3000){
    break;
  }
}
console.log("Hi");

很抱歉,答案還是一樣,等到 3 秒後會印出 Hi,接著才會再印出 0sec

咦,答錯的只有我嗎,你們是不是都答對了呢~

可能觀念還是有點抽象,還記得我們小時候看過的童話書嗎,劇情大概是這樣:
「大家一起努力工作為了完成某件事情,但有些壞人假裝要來工作,其實是來搞破壞的,造成大家進度 delay。國王得知後震怒,下令以後這些壞人一出現,都先把他們隔離,等到他們真的要幹正事時,才能夠加入另外一條次等排隊通道,等到好人們把事情都做完,他們才能夠按照排隊順序進場工作。」

Hmm...我沒有看過這種童話故事,退訂!

且慢,讓我把故事中的一些用語替換掉,

  • 壞人 => setTimeouteventUI RenderAjax、...。
  • 幹正事 => 執行 callback function
  • 次等排隊通道 => 事件佇列(Queue)

這樣是不是比較清楚了呢?

那我們把剛剛的範例流程列出來,分為兩個主軸,
setTimeout:

  1. setTimeout 被放到旁邊倒數(不管幾秒都會先被拉出來)。
  2. 倒數完畢,callback function 放到 事件佇列(Queue)

while 等待3秒:

  1. 執行同步執行的程式碼
  2. 印出 Hi

等到 主執行緒 執行完所有同步執行的程式碼時,才會去查看 事件佇列 查看有沒有需要被執行的任務,也就是直到這一刻才會印出 0sec

觀察到另外一個重點了嗎?
setTimeoutsetInterval 設定的等待時間,並不能夠確保它真的會在設定的時間到就馬上執行,也就是假設時間為 10sec,這樣只能夠確保它會在 大於等於 10sec 後才會執行」。

都了解以後再看看這個問題,印出的順序為何呢?

setTimeout(function(){console.log("0sec")}, 0);
setTimeout(function(){console.log("2sec")}, 2000);
setTimeout(function(){console.log("4sec")}, 4000);

var now = Date.now();
while(true){
  if(Date.now() > now + 3000){
    break;
  }
}
console.log("Hi");
setTimeout(function(){console.log("3sec")}, 3000);
setTimeout(function(){console.log("0sec")}, 0);

聰明的你應該答對了,
Hi
0sec
2sec
0sec
4sec
3sec

運作流程為:

  1. 0、2、4 秒拉出倒數 (倒數完成後 callback function 丟入 事件佇列)
  2. while 等待3秒
  3. 印出 Hi
  4. 3、0 秒拉出倒數 (倒數完成後 callback function 丟入 事件佇列)
  5. 主執行緒空了 => 查看事件佇列
  6. 印出 0、2sec (4sec 還在倒數)
  7. 印出 while 下方的 0sec
  8. 印出 4sec
  9. 印出 3sec

小結

開發時要避免執行時間過長的同步程式碼,因為不只 setTimeoutsetInterval,還有其他非同步處理事件,像是 EventUI Render

請問如果在 3 秒內觸發 click 事件,會發生什麼事...?

document.body.addEventListener('click', function(){
  console.log("Clike!");
});

var now = Date.now();
while(true){
  if(Date.now() > now + 3000){
    break;
  }
}
console.log("Hi");

沒錯,在 3 秒內觸發的 click 並不會馬上印出 Click!,而是會等到 3 秒後印出 Hi 時,才會印出。

同樣的問題還有這個,按直覺來說,應該會先把 body 清空之後,才會印出 Hi 吧?

document.body.innerHTML = "";

var now = Date.now();
while(true){
  if(Date.now() > now + 3000){
    break;
  }
}
console.log("Hi");

答錯了,因為渲染的事件也被丟到非同步處理,所以會等所有同步的程式碼執行完才會觸發!


今天的分享嘗試使用不同的寫法,想讓冰冷的文字多了那麼一點的溫度,試著不讓內容太過枯燥。
但有些用語可能太過口語化,或是用詞沒有那麼精準,若是有些地方沒解釋清楚,不喜歡的朋友別急著左轉,您可以在下方補充較詳細的說明喔。

那今日的分享到這,我們明天見~/images/emoticon/emoticon51.gif


上一篇
Day4 Hoisting & Scope Chain
下一篇
Day6 工具包: 函數&模組化(1)
系列文
從0.5開始的JavaScript30

1 則留言

0
暐翰
iT邦大師 2 級 ‧ 2018-10-05 18:41:57

好生動的描述! /images/emoticon/emoticon12.gif

hbdoy iT邦新手 5 級‧ 2018-10-05 22:46:57 檢舉

謝謝/images/emoticon/emoticon42.gif

我要留言

立即登入留言