在學習 JS 時,必須要知道它是單執行緒(單線程),最好理解的方法就是它一次只能做一件事。
那這就怪了,同步執行下,setTimeout、event callback、Http request 這些耗時、不確定觸發執行時間的操作,不就會使程式阻塞,都在等它們就飽了?
所幸可以透過同步/非同步來處理這些事件,將這些耗時、無法預期執行時間的操作以非同步處理,等執行完同步的程式碼再來處理它們。
真是可喜可賀可喜可賀~
Hmm...不對吧,你不是說 JS 是同步執行,哪來的非同步?
抓到了,你再胡說我要退訂了!
等等,讓我解釋清楚,不要讓已經夠少的訂閱歸零Q_Q
先記得一個重點: JS 的引擎是單執行緒的,瀏覽器無論在什麼時候都只有一個線程在執行 JS 程式。
這邊引用知乎上面的問答(來源找不到,歡迎幫忙補上),並盡量修改成台灣的用語,
瀏覽器的內核是多線程的,它們在內核控制下相互配合以保持同步,至少實現三個常駐線程: 1. JavaScript 引擎線程、2. GUI 渲染線程、3. 瀏覽器事件觸發線程。
3.事件觸發線程: 當一個事件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待 JavaScript 引擎的處理。
這些事件可來自 JavaScript 引擎當前執行的程式碼,如 setTimeout、也可來自瀏覽器內核的其他線程如滑鼠點擊、Ajax 異步請求等,但由於 JavaScript 的單線程關係,所有這些事件都得排隊等待 JavaScript 引擎處理(當線程中沒有執行任何同步程式碼的前提下才會執行異步程式碼)。
看到上面的描述,我們統整出兩個重點,
setTimeout、event、ajax,或其他無法預期執行時間的操作,都會以非同步處理。也就是會先被丟到事件佇列(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...我沒有看過這種童話故事,退訂!
且慢,讓我把故事中的一些用語替換掉,
setTimeout、event、UI Render、Ajax、...。執行 callback  function
事件佇列(Queue)
這樣是不是比較清楚了呢?
那我們把剛剛的範例流程列出來,分為兩個主軸,setTimeout:
setTimeout 被放到旁邊倒數(不管幾秒都會先被拉出來)。callback function 放到 事件佇列(Queue)。while 等待3秒:
Hi
等到 主執行緒 執行完所有同步執行的程式碼時,才會去查看 事件佇列 查看有沒有需要被執行的任務,也就是直到這一刻才會印出 0sec。
觀察到另外一個重點了嗎?
「setTimeout、setInterval 設定的等待時間,並不能夠確保它真的會在設定的時間到就馬上執行,也就是假設時間為 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
運作流程為:
callback function 丟入 事件佇列)callback function 丟入 事件佇列)開發時要避免執行時間過長的同步程式碼,因為不只 setTimeout、setInterval,還有其他非同步處理事件,像是 Event、UI 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");
答錯了,因為渲染的事件也被丟到非同步處理,所以會等所有同步的程式碼執行完才會觸發!
今天的分享嘗試使用不同的寫法,想讓冰冷的文字多了那麼一點的溫度,試著不讓內容太過枯燥。
但有些用語可能太過口語化,或是用詞沒有那麼精準,若是有些地方沒解釋清楚,不喜歡的朋友別急著左轉,您可以在下方補充較詳細的說明喔。
那今日的分享到這,我們明天見~