前言
各類詳細的Promise
教程已經滿天飛了,我寫這一篇也只是用來自己用來總結和整理用的。如果有不足之處,歡迎指教。
為什麼我們要用Promise
JavaScript語言的一大特點就是單執行緒。單執行緒就意味著,所有任務需要排隊,前一個任務結束,才會執行後一個任務。
為了解決單執行緒的堵塞問題,現在,我們的任務可以分為兩種,一種是同步任務(synchronous),另一種是非同步任務(asynchronous)。
- 同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;
- 非同步任務指的是,不進入主執行緒、而進入”任務佇列”(task queue)的任務,只有”任務佇列”通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。
非同步任務必須指定回撥函式,當主執行緒開始執行非同步任務,就是執行對應的回撥函式。而我們可能會寫出一個回撥金字塔,維護大量的callback將是一場災難:
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// ...
});
});
});
});
複製程式碼
而Promise 可以讓非同步操作寫起來,就像在寫同步操作的流程,而不必一層層地巢狀回撥函式。
(new Promise(step1))
.then(step2)
.then(step3)
.then(step4);
複製程式碼
簡單實現一個Promise
關於Promise
的學術定義和規範可以參考Promise/A+規範,中文版【翻譯】Promises/A+規範。
Promise有三個狀態pending
、fulfilled
、rejected
:
三種狀態切換隻能有兩種途徑,只能改變一次:
- 非同步操作從未完成(pending) => 成功(fulfilled)
- 非同步操作從未完成(pending) => 失敗(rejected)
Promise 例項的then
方法,用來新增回撥函式。
then
方法可以接受兩個回撥函式,第一個是非同步操作成功時(變為fulfilled
狀態)時的回撥函式,第二個是非同步操作失敗(變為rejected
)時的回撥函式(該引數可以省略)。一旦狀態改變,就呼叫相應的回撥函式。
下面是一個寫好註釋的簡單實現的Promise的實現:
class Promise {
constructor(executor) {
// 初始化state為pending
this.state = `pending`
// 成功的值
this.value = undefined
// 失敗的原因
this.reason = undefined
// 非同步操作,我們需要將所有then中的成功呼叫儲存起來
this.onResolvedCallbacks = []
// 非同步操作,我們需要將所有then中的失敗呼叫儲存起來
this.onRejectedCallbacks = []
let resolve = value => {
// 檢驗state狀態是否改變,如果改變了呼叫就會失敗
if (this.state === `pending`) {
// resolve呼叫後,state轉化為成功態
this.state = `fulfilled`
// 儲存成功的值
this.value = value
// 執行成功的回撥函式
this.onResolvedCallbacks.forEach(fn => fn)
}
}
let reject = reason => {
// 檢驗state狀態是否改變,如果改變了呼叫就會失敗
if (this.state === `pending`) {
// reject呼叫後,state轉化為失敗態
this.state === `rejected`
// 儲存失敗的原因
this.reason = reason
// 執行失敗的回撥函式
this.onRejectedCallbacks.forEach(fn => fn)
}
}
// 如果executor執行報錯,直接執行reject
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
// then 方法 有兩個引數onFulfilled onRejected
then(onFulfilled, onRejected) {
// 狀態為fulfilled,執行onFulfilled,傳入成功的值
if (this.state === `fulfilled`) {
onFulfilled(this.value)
}
// 狀態為rejected,執行onRejected,傳入失敗的原因
if (this.state === `rejected`) {
onRejected(this.reason)
}
// 當狀態state為pending時
if (this.state === `pending`) {
// onFulfilled傳入到成功陣列
this.onResolvedCallbacks.push(()=>{
onFulfilled(this.value);
})
// onRejected傳入到失敗陣列
this.onRejectedCallbacks.push(()=>{
onRejected(this.reason);
})
}
}
}
複製程式碼
如果需要實現鏈式呼叫和其它API,請檢視下面參考文件連結中的手寫Promise教程。
優雅的使用Promise
使用Promise封裝一個HTTP請求
function get(url) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open(`GET`, url);
req.onload = function() {
if (req.status == 200) {
resolve(req.responseText);
}
else {
reject(Error(req.statusText));
}
};
req.onerror = function() {
reject(Error("Network Error"));
};
req.send();
});
}
複製程式碼
現在讓我們來使用這一功能:
get(`story.json`).then(function(response) {
console.log("Success!", response);
}, function(error) {
console.error("Failed!", error);
})
// 當前收到的是純文字,但我們需要的是JSON物件。我們將該方法修改一下
get(`story.json`).then(function(response) {
return JSON.parse(response);
}).then(function(response) {
console.log("Yey JSON!", response);
})
// 由於 JSON.parse() 採用單一引數並返回改變的值,因此我們可以將其簡化為:
get(`story.json`).then(JSON.parse).then(function(response) {
console.log("Yey JSON!", response);
})
// 最後我們封裝一個簡單的getJSON方法
function getJSON(url) {
return get(url).then(JSON.parse);
}
複製程式碼
then()
不是Promise的最終部分,可以將各個then
連結在一起來改變值,或依次執行額外的非同步操作。
Promise.then()的非同步操作佇列
當從then()
回撥中返回某些內容時:如果返回一個值,則會以該值呼叫下一個then()
。但是,如果返回類promise
的內容,下一個then()
則會等待,並僅在 promise 產生結果(成功/失敗)時呼叫。
getJSON(`story.json`).then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
console.log("Got chapter 1!", chapter1);
})
複製程式碼
錯誤處理
then()
包含兩個引數onFulfilled
, onRejected
。onRejected
是失敗時呼叫的函式。
對於失敗,我們還可以使用catch
,對於錯誤進行捕捉,但下面兩段程式碼是有差異的:
get(`story.json`).then(function(response) {
console.log("Success!", response);
}, function(error) {
console.log("Failed!", error);
})
get(`story.json`).then(function(response) {
console.log("Success!", response);
}).catch(function(error) {
console.log("Failed!", error);
})
// catch 等同於 then(undefined, func)
get(`story.json`).then(function(response) {
console.log("Success!", response);
}).then(undefined, function(error) {
console.log("Failed!", error);
})
複製程式碼
兩者之間的差異雖然很微小,但非常有用。Promise 拒絕後,將跳至帶有拒絕回撥的下一個then()
(或具有相同功能的 catch()
)。如果是 then(func1, func2)
,則 func1
或 func2
中的一個將被呼叫,而不會二者均被呼叫。但如果是 then(func1).catch(func2)
,則在 func1
拒絕時兩者均被呼叫,因為它們在該鏈中是單獨的步驟。看看下面的程式碼:
asyncThing1().then(function() {
return asyncThing2();
}).then(function() {
return asyncThing3();
}).catch(function(err) {
return asyncRecovery1();
}).then(function() {
return asyncThing4();
}, function(err) {
return asyncRecovery2();
}).catch(function(err) {
console.log("Don`t worry about it");
}).then(function() {
console.log("All done!");
})
複製程式碼
以下是上述程式碼的流程圖形式:
藍線表示執行的 promise 路徑,紅路表示拒絕的 promise 路徑。與 JavaScript 的 try/catch 一樣,錯誤被捕獲而後續程式碼繼續執行。
並行和順序:兩者兼得
假設我們獲取了一個story.json
檔案,其中包含了文章的標題,和段落的下載地址。
1. 順序下載,依次處理
getJSON(`story.json`).then(function(story) {
addHtmlToPage(story.heading);
return story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Once the last chapter`s promise is done…
return sequence.then(function() {
// …fetch the next chapter
return getJSON(chapterUrl);
}).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
// And we`re all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector(`.spinner`).style.display = `none`;
})
複製程式碼
2. 並行下載,完成後統一處理
getJSON(`story.json`).then(function(story) {
addHtmlToPage(story.heading);
// Take an array of promises and wait on them all
return Promise.all(
// Map our array of chapter urls to
// an array of chapter json promises
story.chapterUrls.map(getJSON)
);
}).then(function(chapters) {
// Now we have the chapters jsons in order! Loop through…
chapters.forEach(function(chapter) {
// …and add to the page
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}).catch(function(err) {
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector(`.spinner`).style.display = `none`;
})
複製程式碼
3. 並行下載,一旦順序正確立即渲染
getJSON(`story.json`).then(function(story) {
addHtmlToPage(story.heading);
// Map our array of chapter urls to
// an array of chapter json promises.
// This makes sure they all download parallel.
return story.chapterUrls.map(getJSON)
.reduce(function(sequence, chapterPromise) {
// Use reduce to chain the promises together,
// adding content to the page for each chapter
return sequence.then(function() {
// Wait for everything in the sequence so far,
// then wait for this chapter to arrive.
return chapterPromise;
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector(`.spinner`).style.display = `none`;
})
複製程式碼
async / await
async
函式返回一個 Promise 物件,可以使用then
方法新增回撥函式。當函式執行的時候,一旦遇到await
就會先返回,等到非同步操作完成,再接著執行函式體內後面的語句。
基本用法
我們可以重寫一下之前的getJSON
方法:
// promise 寫法
function getJSON(url) {
return get(url).then(JSON.parse).catch(err => {
console.log(`getJSON failed for`, url, err);
throw err;
})
}
// async 寫法
async function getJSON(url) {
try {
let response = await get(url)
return JSON.parse(response)
} catch (err) {
console.log(`getJSON failed for`, url, err);
}
}
複製程式碼
注意:避免太過迴圈
假定我們想獲取一系列段落,並儘快按正確順序將它們列印:
// promise 寫法
function chapterInOrder(urls) {
return urls.map(getJSON)
.reduce(function(sequence, chapterPromise) {
return sequence.then(function() {
return chapterPromise;
}).then(function(chapter) {
console.log(chapter)
});
}, Promise.resolve())
}
複製程式碼
*不推薦的方式:
async function chapterInOrder(urls) {
for (const url of urls) {
const chapterPromise = await getJSON(url);
console.log(chapterPromise);
}
}
複製程式碼
推薦寫法:
async function chapterInOrder(urls) {
const chapters = urls.map(getJSON);
// log them in sequence
for (const chapter of chapters) {
console.log(await chapter);
}
}
複製程式碼