
Event Table
Event Table 是與Event Queue 互相搭配的資料集合,它負責記錄在非同步目的達成後,有哪些函式或者事件要被執行,這裡指的非同步目的指的是像計時完畢、API資料獲取完畢、事件被觸發。當我們執行setTimeout這個函式時,JavaScript會把給定的函式與像是倒數的秒數之類的附帶資訊(meta data)推送到Event Table裡面,等到一秒過後(目的達成)該函式教會被正式推送到事件除列等待執行,而在這之前JavaScript就是透過Event Table知道有那些事件要被送到事件儲列中。
Event Loop
那麼,什麼又是事件迴圈Event Loop呢?可以把Event Loop想成是另外一個幾乎無時無刻、每一毫秒都在執行的程式,它負責檢查現在主執行環境堆疊是否是空的。如果是空的,再去檢查Event Queue,若Event Queue有函式等待執行,則將這些函式從Event Queue依序到主執行環境並執行。

也因為有事件迴圈與事件儲列的機制,像是事件監聽與透過Ajax拉取資料這類行為才有辦法被達成。例如在監聽網頁點時,一旦使用者做了點擊的動作,對應的邏輯就會被推送到事件儲列內,而事件迴圈看到儲列內有待執行的任務,就會負責去執行。
那麼,現在了解了整個事件儲列的概念,讓我們再度回到setTimeout的例子,來看看一道經典的面試問題。
for (var i=0; i<3; i++){
setTimeout(function(){
console.log(i)
}, 1000)
}
各位可以先想一下,在一秒之後,setTiomeout函式內console.log的輸出的結果是甚麼。這邊用到的概念跟前面講閉包的時候類似,都與先後順序有關。若沒有非同步概念的話,通常會下意識的認為結果會是0、1、2,不過這段程式碼最後其實是會印出3、3、3,不知道有沒有猜對。
搭配識見儲列的概念,現在我們知道setTimeout對應的函式會在主執行環境結束之後才被執行,而等到該函式被執行時,for迴圈裡面的i早就已經因為迴圈而被修改為3了。

要改變三個3的結果,想要看到0、1、2的話該怎麼辦呢?這邊要結合一點前面說到的範疇與閉包的觀念,可以看到for迴圈裡面的i是利用var所宣告,而既然var具有的範疇是根據函式來界定,那麼我只要利用函式,是不是就是夠透過產生閉包,把每個迴圈的i值保留下來呢?當然可以,我們可以利用立即執行函式(IIFE),並把i導入這個立即執行函式,就能夠產生閉包了。
for (var i=0; i<3; i++)
(function(x) {
setTimeout(function(){
console.log(x)
})(i)
}
或者是把var宣告改成let來做宣告
for (let i=0; i<3; i++){
setTimeout(function(){
console.log(i)
}, 1000)
}
Promise
我們在前一個段落提到,為了防止網頁的主程式因為等待某些邏輯運算的回應而停擺,有時候JavaScript的行為需要透過非同步的方式來執行,包括一些瀏覽器的API和對外部伺服器拉取資料的動作,而這些動作是利用瀏覽器的Event Queue來達成非同步的行為。
這的確減少不必要的等待,進而增加了使用流程上的順暢度。不過就程式碼的撰寫方式來看,由於在做這些非同步行為的時候,若不做任何的處理,一般都是以回呼函式的方式來進行,才能確保某一段邏輯在非同步行為完成之後才被執行,若這類邏輯開始複雜的時候,可能會變得難以閱讀。
舉前例講到的例子來說,若以setTimeout來模擬一個非同步的行為,想要確保這件事情的話,我們就必須把一個非同步行為放到另外一個非同步的行為的回呼函式裡面。當這樣子的需求越來越多時,你可能就會看到這樣的程式碼。
const asynActionA = (fn)=>setTimeout(fn, 10000);
const asynActionB = (fn)=>setTimeout(fn, 10000);
const asynActionC = (fn)=>setTimeout(fn, 10000);
const asynActionD = (fn)=>setTimeout(fn, 10000);
asyncActionA(()=>{
console.log("asynActionA");
asynActionB(()=>{
console.log("asynActionB");
asynActionC(()=>{
console.log("asynActionC");
asynActionD(()=>{
console.log("asynActionD");
});
});
});
});
這種巢狀的回呼函式一旦多了起來,就容易造成維護上的困難。一般在正式的專案中都非常不樂見這樣子的程式碼。為了解決這樣的問題,我們就必須認識這個Promise。
>Promise簡介
Promise是什麼呢?Promise是JavaScript版本E86以後出現的新語法,這個詞以字面上的意義來看,用比較白話的方式解釋的話有一種:我承諾幫你做某件事情,能不能成功還不一定,但是我做完之後會把結果告訴你的意思。官方的文件描述則是:
Promise是一個代表非同步運作的最終狀態的物件(成功或失敗)
A Promise is an object repressnting the eventual completion
or failure of an asynchronous operation.(MDN)
從技術文件的角度來解釋就顯得比較抽象,不過你應該大致能夠看出一點頭緒,只要抓住幾個關鍵字--也就是「成功」與「失敗」兩種狀態。
>Promise的狀態
進一步總結以上的論點,一個Promise以時間順序來看是有狀態之分的。除了前面講的成功與失敗兩種結果,一般以Pending來描述在執行中,懸而未決的Promise,一個Promise總共會有三種可能的狀態,分別代表進行中、成功與失敗。而相對於Pending這個代表處理中的狀態,不管是進入成功或失敗狀態的Promise,我們都能夠用Settled來表示這個Promise已經被解決了。
*Pending:還在執行中的狀態,表示還沒有特定結果。
*Fulfilled:成功的狀態,代表Promise被實現,對應的回呼函式為resolve。
*Rejected:失敗的狀態,代表Promise被拒絕,對應的回呼函式為reject。
*Settled:表示Promise已經被解決,結果已經確定。
>Promise基本使用方式
從前面文件的描述應該也可以看得出來Promise在JavaScript裡面是以物件的方式存在。這個物件又是怎麼產生呢?接下來我們就來看看要怎麼使用Promise吧!基本的Promise宣告方式如下:

我們進一步仔細看一下這個傳進Promise建構函式,它接收了兩個參數:resolve和reject來命名。

resolve和reject 其實分別是兩個具有不同目的的函式。resolve(解析)被用於在認為Promise內的行為成功時呼叫;reject(拒絕)則用於被認為失敗的邏輯發生時呼叫。使用這兩個詞最為函式的名稱只因為這是一種約定俗成,許多人都用這些詞來稱呼它們。