特別説明
- 這篇博客是我個人對
JavaScript 異步操作的總結歸類。 - 通過這篇文章我也希望讀者可以從
宏觀的角度看待 JavaScript 異步操作是如何演化的。 - 但是如果想要通過這篇博客全面掌握
promise或者async函數等其他技術的全部知識,還是不太現實的。 - 推薦大家精讀阮一峯老師的 ECMAScript 6 入門 - Promise 對象 ,和尼古拉斯老師的《深入理解 ES6》的第十一章(Page 219)。
摘要
用有一點哲學調調的話説:JavaScript 異步操作進化的終極目標就是讓異步的代碼
看起來更像同步的代碼。
JS 這門語言單線程運作的天性,並沒有讓使用它的人覺得它是雞肋的,反而讓程序
員們創造出了各式各樣的工具來提高它的性能。
從回調函數到 Promise 對象,再到被認為是 JS 異步操作最終解決方案的 async 函數。
這其中的每一次進化都是從無到有,從社區到標準。
這篇博客將從源頭出發,先探討一下為什麼 JS 需要異步操作。接着再解釋一些概念性的
名詞。最後捋一捋 JS 異步操作的發展過程。
關鍵詞
Synchronous | Asynchronous | Event Loop | CallBack | Promise | Generator | Async/Await
JavaScript 為什麼需要異步操作
單線程
- JavaScript 這麼語言設計的初衷是為了解決用户與瀏覽器的交互問題。
- 這其中有一項重頭戲就是 DOM 操作。試想一下某個代碼塊是在修改 DOM,另外還有一個代碼塊需要刪除 DOM。那麼應該聽誰的?
- 為了避免出現比較複雜的線程同步問題,JS 執行環境中負責執行代碼的線程只有一個。這就是我們常説的 JavaScript 單線程工作模式。
工作模式
- 有的時候在執行一些很耗時的任務時,就需要等待當前任務執行完才能進入下一個任務。這樣程序就會出現假死現象,也就是我們常説的阻塞。
- 為了避免這樣的情況發生,JS 將工作模式主要分為兩類:同步模式和異步模式。
一些概念
Synchronous
同步模式執行的代碼會在執行棧中排隊執行。也就是我們常説的壓棧運行,當運行完了以後就會被彈出棧。閉包函數可以把某一變量持久保存在執行棧中。
Asynchronous
異步模式的代碼不會進入主線程,也就是我們的執行棧。而是進入任務隊列或者説消息隊列中。當 執行棧 中所有 同步任務 執行完畢,系統就會去讀取 任務隊列 ,那些 異步代碼 就會結束等待,進入執行棧,開始執行。
Note:同步還是異步是指運行環境提供的 API 是以 同步 或 異步 模式的方式工作。
同步 API:console.log()
異步 API:setTimeOut()
Stack
執行棧。主線程運行的時候,產生堆和棧,棧中的代碼調用各種外部 API,它們在任務隊列中加入各種事件。
Message queue
消息隊列。
Web API
瀏覽器所提供的各種 API 接口。
Event loop
只要棧中的代碼執行完畢,主線程就會從消息隊列中讀取異步操作,這個過程是循環不斷的,所以整個的這種運行機制又稱為事件循環 -- Event Loop。
關於 stack、message queue、event loop 和 web api 的關係可參考下圖:
進化史
CallBack
回調函數是最早的異步操作實現方式。它是由調用者定義,交給執行者執行的函數。幾種常見的應用有:事件機制和發佈-訂閲模式等。
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function () { ... } // 在接收到完整的響應數據時觸發的回調函數
xhr.onerror = function () { ... } // 在請求發送錯誤時觸發的回調函數
xhr.send()
缺陷:當我們需要發送多個請求,並且要在這些請求全部都返回成功的時候去對所有的請求結果做某些處理時,不免要想一些特殊的技巧。最簡單的方式就是把每個請求都嵌套起來,當一個請求成功時再去執行下一個請求。這樣實現的問題首先會很浪費時間,其次也會形成我們常説的回調地獄的情況,使代碼既不美觀也很難維護。
/* 第一層 */
$.ajax({
url: URL1,
/* callback1 */
success: function () {
/* 第二層 */
$.ajax({
url: URL2,
/* callback2 */
success: function () {
...
/* 第 n 層 */
$.ajax({ ... })
}
})
}
})
Promise
Promise 是一個對象,是為 異步操作 的結果所準備的佔位符。用來表示 異步任務 結束之後是成功還是失敗。Promise 的狀態一旦確定以後,就不可以被修改。
Promise 的生命週期:在執行 異步操作 時,會承諾給出一個結果,在最後給出結果之前叫做 pending 狀態,給出的結果有兩種,成功的 fulfilled 狀態和失敗的 rejected 狀態。在給出結果之後需要作出一些反應(交代任務),與之對應的就是 onFulfilled 和 onRejected。
-
then() 方法:可以使用 then() 方法在 Promise 的狀態改變時執行一些特定操作。Promise 的本質是使用回調函數定義異步任務結束後所需執行的任務。
function ajax (url) { return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest() xhr.responseType = 'json' xhr.onload = function () { if (this.status === 200) { resolve(this.response) } else { reject(new Error(this.statusText)) } } xhr.send() }) } ajax('/api/user.json').then(function (res) { console.log(res) }, function (error) { console.log(error) }) -
串聯 Promise- Promise 對象的 then() 方法會返回一個全新的 Promise 對象
- 後面的 then() 方法就是為上一個 then 返回的 Promise 註冊回調
- 前面 then() 方法中回調函數的返回值會作為後面 then() 方法回調的參數
- 如果回調中返回的是 Promise ,那後面的 then() 方法的回調會等待它的結束
- 因此 Promise 是可以進行鏈式調用的。每一個 then() 方法實際上都是在為上一個 then() 方法返回的 Promise 對象添加狀態明確過後的回調。
let p1 = new Promise(function(resolve, reject) { resolve(42); }) p1.then(function (value) { console.log(value) }).then(function () { console.log("Finished") }) // 42 // Finished -
捕獲錯誤- onRejected 回調在 Promise 失敗或者出現異常時都會被執行。
- catch() 方法相當於 then() 方法所接受的第二個參數,但是區別在於 -- catch() 方法在 Promise 鏈中允許我們捕獲前一個 Promise 的完成或拒絕處理函數中發生的錯誤。
let p1 = new Promise(function(resolve, reject) { throw new Error("Explosion") }) p1.catch(function (error) { console.log(error.message) throw new Error("Boom") }).catch(function (error) { console.log(error.message) }) -
Promise 靜態方法/* Promise.resolve() */ let promise = Promise.resolve(42) promise.then(function (value) { console.log(value) // 42 }) /* Promise.reject() */ let promise = Promise.reject(42) promise.catch(function (value) { console.log(value) // 42 })/* 下面這兩種寫法是等價的 */ Promise.resolve('foo') .then(function (value) { console.log(value) }) new Promise(function (resolve, reject) { resolve('foo') }) -
Promise 並行執行/* Promise.all() * 該方法接收單個可迭代對象(例如數組)作為參數,並返回一個 Promise。 * 所有的可迭代 Promise 元素都完成後,所返回的 Promise 才會被完成。 */ let p1 = new Promise(function (resolve, reject) { resolve(42) }) let p2 = new Promise(function (resolve, reject) { reject(43) }) let p = Promise.all([p1, p2]) p.catch(function (value) { console.log(value) // 43 })/* Promise.race() * 該方法也接收一個 Promise 可迭代對象,並返回一個新的 Promise。 * 一旦來源 Promise 中有一個被解決,所返回的 Promise 就會立即被解決。 */ let p1 = Promise.resolve(42) let p2 = new Promise(function (resolve, reject) { resolve(43) }) let p = Promise.race([p1, p2]) p.then(function (value) { console.log(value) // 42 })
宏任務和微任務
回調隊列中的任務被稱為宏任務。宏任務執行過程中,可以臨時加上一些額外需求。可以選擇作為一個 新的宏任務 進入任務隊列排隊,也可以作為 當前任務 的微任務,直接在當前任務結束過後立即執行。微任務 可以提高整體的響應能力,Promise 的回調會被作為微任務執行。可以使用 setTimeOut() 添加 宏任務。
console.log("global start")
setTimeOut(() => {
console.log("setTimeOut")
}, 0)
Promise.resolve()
.then(() => {
console.log("Promise")
})
.then(() => {
console.log("Promise2")
})
console.log("global end")
// global start
// global end
// Promise
// Promise2
// setTimeOut
Generator
Generator 生成器執行過程:
- 定義時在函數名前面有一個 * 號
- 在調用 Generator 函數時並不會立即去執行這個函數,而是會得到一個生成器對象
- 當我們調用這個 生成器對象 的 next() 方法時才會去執行
- 一直會執行到 yield 關鍵字所在的位置,並且把 yield 後面的值 返回出去,然後這個函數就會暫停執行。yield 返回值 會被接收到,形式是
{ value: "foo", done: false } - 當我們再次調用 next() 方法,並且傳入參數,那麼函數就會繼續往下執行,並且我們傳入的參數會作為 yield 的返回值
-
如果我們在外面調用的是生成器對象的 throw() 方法,那麼函數將會得到這個異常。可以在函數內部使用
try...catch...的方式捕獲異常function * main () { const users = yield ajax('/api/users.json') console.log(users) const posts = yield ajax('/api/posts.json') console.log(posts) } const g = main() const result = g.next() result.value.then(data => { const result2 = g.next(data) if (result2.done) return result2.value.then(data => { ... }) })
Async/Await
-
執行 async 函數,返回的都是 Promise 對象async function test1 () { return 1 } async function test2 () { return Promise.resolve(2) } const result1 = test1() const result2 = test2() console.log('result1', result1) console.log('result1', result1) -
Promise.then() 成功的情況,對應 awaitasync function test3 () { const p3 = Promise.resolve(3) p3.then(data => { console.log('data', data) }) const data = await p3 console.log('data', data) }async function test4 () { const data4 = await 4 console.log('data4', data4) } async function test5 () { const test5 = await test1() console.log('test5', test5) } -
Promise.catch() 異常的情況,對應 try...catch有時我們希望即使前一個(異步)操作失敗,也不要中斷後面的(異步)操作。這時就可以使用
try...catch...來捕獲異常async function test6 () { const p6 = Promise.reject(6) try { const data6 = await p6 console.log('data6', data6) } catch (e) { console.log('e', e) } }
鳴謝
- 感謝每一位為 JavaScript 做出貢獻的 programmer。也感謝每位正在做出努力的“潛伏者們”,期待着你們的爆發。
- 阮一峯 老師
- 尼古拉斯 老師
- B 站 “IT課程大拿”