各種詳細的Promise
教程已經滿天飛了,我寫這一篇也只是用來本身用來總結和整理用的。若是有不足之處,歡迎指教。javascript
JavaScript語言的一大特色就是單線程。單線程就意味着,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。html
爲了解決單線程的堵塞問題,如今,咱們的任務能夠分爲兩種,一種是同步任務(synchronous),另外一種是異步任務(asynchronous)。java
異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數。而咱們可能會寫出一個回調地獄。es6
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// ...
});
});
});
});
複製代碼
而Promise 可讓異步操做寫起來,就像在寫同步操做的流程,而沒必要一層層地嵌套回調函數。web
(new Promise(step1))
.then(step2)
.then(step3)
.then(step4);
複製代碼
關於Promise
的學術定義和規範能夠參考Promise/A+規範,中文版【翻譯】Promises/A+規範。json
Promise有三個狀態pending
、fulfilled
、rejected
: 三種狀態切換隻能有兩種途徑,只能改變一次:promise
Promise 實例的then
方法,用來添加回調函數。bash
then
方法能夠接受兩個回調函數,第一個是異步操做成功時(變爲fulfilled
狀態)時的回調函數,第二個是異步操做失敗(變爲rejected
)時的回調函數(該參數能夠省略)。一旦狀態改變,就調用相應的回調函數。app
下面是一個寫好註釋的簡單實現的Promise的實現:異步
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class AJPromise {
constructor(executor) {
this.state = PENDING
this.value = undefined
this.reason = undefined
this.onResolvedCallbacks = []
this.onRejectedCallbacks = []
let resolve = value => {
// 確保 onFulfilled 異步執行
setTimeout(() => {
if (this.state === PENDING) {
this.state = FULFILLED
this.value = value
// this.onResolvedCallbacks.forEach(fn => fn)
// 能夠將 value 操做後依次傳遞
this.onResolvedCallbacks.map(cb => (this.value = cb(this.value)))
}
})
}
let reject = reason => {
setTimeout(() => {
if (this.state === PENDING) {
this.state = REJECTED
this.reason = reason
// this.onRejectedCallbacks.forEach(fn => fn)
this.onRejectedCallbacks.map(cb => (this.reason = cb(this.reason)))
}
})
}
try {
//執行Promise
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
then(onFulfilled, onRejected) {
if (this.state === FULFILLED) {
onFulfilled(this.value)
}
if (this.state === REJECTED) {
onRejected(this.reason)
}
if (this.state === PENDING) {
typeof onFulfilled === 'function' &&
this.onResolvedCallbacks.push(onFulfilled)
typeof onRejected === 'function' &&
this.onRejectedCallbacks.push(onRejected)
// 返回 this 支持then方法能夠被同一個 promise 調用屢次
return this
}
}
}
複製代碼
若是須要實現鏈式調用和其它API,請查看下面參考文檔連接中的手寫Promise教程。
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
連接在一塊兒來改變值,或依次運行額外的異步操做。
當從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
文件,其中包含了文章的標題,和段落的下載地址。
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';
})
複製代碼
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';
})
複製代碼
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
函數返回一個 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);
}
}
複製代碼