年前有個朋友面試,遇到了同步和非同步的問題。這幾年 JavaScript 也從 callback,慢慢演進到許多非同步的解法。

之前有提過 JavaScript 是一門有點 FP(Functional Progrmming)的語言,而 FP 把函式當成 first class(一等公民),所有的 function 都可以當成一種物件、當成一種參數來傳遞。像是你可以寫出這樣子的程式碼:

const onSuccess = (res) => {
    if (res.result === 0) {
        console.log('登入成功');
    } else {
        console.log('登入失敗');
    }
};

login('username', 'password', onSuccess);
}

但你可能會比較習慣 callback 的形式,直接把那個 function 放在參數裡面,不另行宣告:

login('username', 'password', (res) => {
    if (res.result === 0) {
        console.log('登入成功');
    } else {
        console.log('登入失敗');
    }
});

callback 是個很棒的寫法,可以很容易地看出哪個 function 執行完之後要做什麼事情。不過 callback 一多,縮排就會很醜:

login('username', 'password', (res) => {
    if (res.result === 0) {
        getPosts(res.uid, (posts) => {
            if (posts && posts.length) {
                console.log('你的文章有 ' + posts.length + ' 篇');
            } else {
                console.log('沒有文章');
            }
        });
    } else {
        console.log('登入失敗');
    }
});

然後再多幾個 callback 就會變成龜派氣功了。這就是俗稱的 Callback Hell。

cb_hell

這裡(callbackhell.com) 有幾個做法教你如何避免 Callback Hell,像是宣告好函式然後拆開使用之類的。不過接下來要講的 Promise,才是 JS 試圖解決 Callback Hell 的方法。

Promise

有了 Promise 以後,你就可以把剛剛那段 code 改寫成這樣:

loginAsync('username', 'password')
.then((res) => {
    if (res.result === 0) 
        return getPostAsync(res.uid);
    return console.log('登入錯誤');
})
.then((posts) => {
    if (posts && posts.length) {
        console.log('你的文章有 ' + posts.length + ' 篇');
    } else {
        console.log('沒有文章');
    }
});

可以像發動遊戲王卡的陷阱卡一樣一步一步串(chain)起來,只要用一個 Promise 把原先的動作包起來就好了。

而寫一個簡單的 Promise 可以這樣做:

const logAsync = (message, time) => {
    return new Promise((resolve, reject) => {
        if (message && time) {
            setTimeout(() => {
                console.log(message);
                resolve()
            }, time);
        } else {
            reject();
        }
    });
};

主要是回傳一個 PromisePromise 裡面放著要執行的 function,然後成功的話呼叫 resolve 方法、失敗的話呼叫 reject 方法。這樣就可以把方法 chain 起來:

logAsync('這個訊息過一秒才會出現', 1000)
.then(() => {
  return logAsync('這個訊息再過 1.5 秒才會出現', 1500);
})
.then(() => {
  return logAsync('這個訊息再過 2 秒才會出現', 2000);
});

前往 此篇文章完整版 或到 CodePen 上查看程式碼

原本就存在的 function 可以以類似的方法用 Promise 包起來,這樣就能用 then 的方式連續呼叫多個方法了。

Promise 其實還有不少用法,之後有機會再開番外篇來介紹。這篇提 Promise 主要是為了後面的 async/await 鋪路,這才是 JS 非同步潮的地方。

你可能還聽過 generator,但現在其實很少用到這東西了。建議你學 async/await 就好了。

Async/await

async function 是不管怎樣都會回傳 Promise 的函式。例如:

const foo = async () => {
    return 1;
}

foo().then((res) => {
    console.log(res);
});

雖然我們的 foo 回傳的不是一個 Promise,但因為它是 async function 的關係,JS 會自動把它包成 Promise,所以可以使用 then,結果會得到 1。

await 則是可以等 Promise 執行完再執行下一行:

const demo = async () => {
    await logAsync('1 秒後會出現這句', 1000);
    await logAsync('再 1.5 秒後會出現這句', 1500);
    await logAsync('再 2 秒後會出現這句', 2000);
};

demo();

不過 await 必須在 async function 裡面才能使用。

前往 此篇文章完整版 或到 CodePen 上查看程式碼

而且 await 也能夠把 Promise 回傳的值接起來,通常我們在呼叫 API(例如執行 fetchaxios)的時候就很好用:

(async () => {
  const res = await fetch('API_URL');
  const data = await res.text();
  
  console.log(data);
})();

搭配 axios 更可以這樣使用:

((async () => {
    const { data } = await axios.get('API_URL');
    
    console.log(data);
})();

前往 此篇文章完整版 或到 CodePen 上查看程式碼

結語

使用 async/await 呼叫 API 或是其他非同步方法,不但可以避免 Callback Hell,比起 Promise 更增加了程式可讀性(這點見仁見智就是了),是個我覺得寫 JS 的朋友都應該會的東西。

最後,我覺得如果第一次碰 async/await,還是多看幾篇文章,看看不同人的觀點。我第一次碰這東西也是寫的霧煞煞,所以推薦幾篇文章:

第一篇先到這邊,下一篇來講運算子、選擇結構和函式。

我要學會 JS 目錄