前言
最近我在學習前端相關的知識,沒有意識到的一個問題是我用的Ajax是非同步的,如下面程式碼所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id = "app">
<h2>{{ product }} X are in stock .</h2>
<ul v-for = "product in products" >
<li>
<input type="number" v-model.number="product.quantity">
{{product.name}}
<span v-if = "product.quantity === 0">
已經售空
</span>
<button @click = "product.quantity += 1">
新增
</button>
</li>
</ul>
<h2> 總庫存 {{ totalProducts}}</h2>
</div>
<div>
app.products.pop();
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el:'#app',
data:{
product:'Boots',
products:[
'Boots',
'Jacket'
]
},
computed:{
totalProducts(){
return this.products.reduce((sum,product) => {
return sum + product.quantity;
},0);
}
},
created(){
fetch('http://localhost:8080/hello').then(response=> response.json()).then(json => {
console.log("0");
this.products = json.products;
})
fetch('http://localhost:8080/hello').then(response=> response.json()).then(json => {
console.log("1");
})
}
})
</script>
</body>
</html>
按照我的理解是應該先輸出0,後輸出1。但事實上並非如此,因為fetch是非同步的,這裡的非同步我們可以簡單的理解為執行fetch的執行者並不會馬上執行,所以會出現1先在控制檯輸出這樣的情況:
但我的願望是在第一個fetch任務執行之後再執行第二個fetch任務, 其實可以這麼寫:
fetch('http://localhost:8080/hello').then(response=> response.json()).then(json => {
console.log("0");
this.products = json.products;
fetch('http://localhost:8080/hello').then(response=> response.json()).then(json => {
console.log("1");
})
})
這看起來像是一種解決方案,那如果說我有A、B、C、D四個任務都是非同步的都想按A B C D這樣的順序寫,如果向上面的寫法,巢狀呼叫,那看起來就相當的不體面。由此就引出了promise。
promise
基本使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
new Promise(function(resolve, reject) {
setTimeout(()=>{
console.log(0);
resolve("1");
},3000)
}).then((result)=> console.log(result));
</script>
</html>
如上面程式碼所示,我們宣告瞭一個Promise物件,並向其傳遞了一個函式,函式的行為是3秒後再控制檯輸出0,並呼叫resolve函式,resolve函式會觸發then()。所以我們上面的兩個fetch用promise就可以這麼寫。
fetch('http://localhost:8080/hello').then(response=> response.json()).then(json => {
console.log("0");
this.products = json.products;
}).then(()=>{
fetch('http://localhost:8080/hello').then(response=> response.json()).then(json => {
console.log("1");
})
})
fetch直接返回的就是Promise物件,then之後也還是一個Promise物件。Promise物件的建構函式語法如下:
let promise = new Promise(function(resolve, reject) {
});
對於一個任務來說,任務最終狀態有兩種: 一種是執行成功、一種是執行失敗。引數resolve和reject是由JavaScript自身提供的回撥,是兩個函式, 由 JavaScript 引擎預先定義,因此我們只需要在任務的成功的時候呼叫resolve,失敗的時候reject即可。注意任務的最終狀態只能是成功或者失敗,你不能先呼叫resolve後呼叫reject,這樣任務就會被認定為失敗狀態。注意,如果任務在執行的過程中出現了什麼問題,那麼應該呼叫reject,你可以傳遞給reject任意型別的引數,但是建議使用Error物件或繼承自Error的物件。
Promise的狀態
一個Promise剛建立任務未執行完畢,狀態處於pending。在任務執行完畢呼叫resolve時,狀態轉為fulfilled。任務執行出現異常呼叫error,狀態轉為reject。
then、catch、finally
then的中文意為然後,連貫理解起來也比較自然,任務執行完畢然後執行,語法如下:
promise.then(
function(result) { /* 處理任務正常執行的結果 */ },
function(error) { /* 處理任務異常執行的結果 */ }
);
第一個函式將在 promise resolved 且接收到結果後執行。第二個函式將在 promise rejected 且接收到error資訊後執行。
下面是示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
let promiseSuccess = new Promise(function(resolve,reject){
setTimeout(() => resolve("任務正確執行完畢"),1000);
})
promiseSuccess.then(result=>{
alert(result);
});
let promiseFailed = new Promise(function(resolve,reject){
setTimeout(() => reject("任務執行出現異常"),1000);
})
promiseFailed.then(result=>{
alert(result);
},error=>{
alert(error);
});
</script>
</html>
如果你只想關注任務執行發生異常,可以將null作為then的第一個引數, 也可以使用catch:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
new Promise(function(resolve,reject){
reject("發生了錯誤");
}).catch(error=>{
alert(error);
})
new Promise(function(resolve,reject){
reject("發生了錯誤");
}).then(null,error=> alert(error));
</script>
</html>
像try catch 中的finally子句一樣,promise也有finally,不同於then,finally沒有引數,我們不知道promise是否成功,不過關係,finally的設計意圖是執行“常規”的完成程式。當promise中的任務結束,也就是呼叫resolve或reject,finally將會被執行:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
new Promise(function(resolve,rejetc){
resolve("hello");
}).then(result => alert(result),null).finally(()=>alert("執行清理工作中"));
</script>
</html>
then catch finally 的執行順序為跟在Promise後面的順序。
Promise鏈與錯誤處理
透過上面的討論,我們已經對Promise有一個基本的瞭解了,如果我們有一系列非同步任務,為了討論方便讓我們把它稱之為A、B、C、D,非同步任務的特點是雖然他們在程式碼中的順序是A、B、C、D,但實際執行順序可能四個字母的排列組合,但是我們希望是A、B、C、D,那我們用Promise可以這麼寫:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
new Promise(function(resolve,reject){
alert("我是A任務");
resolve("請B任務執行");
}).then(result=>{
alert(result+"收到請求,B任務開始執行");
return "B 任務執行完畢,請C任務開始執行";
},null).then(result=>{
alert(result+"收到請求,C任務開始執行");
return "C 任務執行完畢,請D任務開始執行";
},null).then(result =>{
alert(result+"D任務開始執行,按A、B、C、D序列執行完畢");
},null)
</script>
</html>
執行流程為: 初始化promise,然後開始執行alert後resolve,此時結果滑動到最近的處理程式then,then處理程式被呼叫之後又建立了一個新的Promise,並將值傳給下一個處理程式,以此類推。隨著result在處理程式鏈中傳遞,我們可以看到一系列alert呼叫:
- 我是A任務
- 請B任務執行收到請求,B任務開始執行
- B 任務執行完畢,請C任務開始執行收到請求,C任務開始執行
- C 任務執行完畢,請D任務開始執行D任務開始執行,按A、B、C、D序列執行完畢
這樣做之所以是可行的,是因為每個對.then的呼叫都會返回了一個新的promise,因此我們可以在其之上呼叫下一個.then。
當處理程式返回一個值時(then),它將成為該promise的result,所以將使用它呼叫下一個.then。
上面的是按A、B、C、D執行,我們也會期待A任務執行之後,B、C、D都開始執行,這類似於跑步比賽,A是開發令槍的人,聽到訊號,B、C、D開始跑向終點。下面是一個示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
let promise = new Promise(function(resolve,reject){
setTimeout(() => resolve(1),1000);
})
promise.then(function(result){
alert(result); // 1
return result * 2;
})
promise.then(function(result){
alert(result); // 1
return result * 2;
})
promise.then(function(result){
alert(result); // 1
return result * 2;
})
</script>
</html>
彈出結果將只會是1。
Promise API
Promise.all
假設我們希望並行執行多個任務,並且等待這些任務執行完畢才進行下一步,Promise為我們提供了Promise.all:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
var proOne = new Promise(resolve=> setTimeout(()=> resolve(1),3000);
var proTwo = new Promise(resolve=> setTimeout(()=> resolve(2),2000);
var proThree = new Promise(resolve=> setTimeout(()=> resolve(3),1000));
Promise.all([proOne,proTwo,proThree]).then(alert);
</script>
</html>
即使proOne任務花費時間最長,但它的結果仍讓是陣列中的第一個。如果人一個promise失敗即呼叫了reject,那麼Promise.all就被理解reject,完全忽略列表中其他的promise:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
var proOne = new Promise(resolve=> setTimeout(()=> resolve(1),1000));
var proTwo = new Promise((resolve,reject)=> setTimeout(()=> reject(2),1000));
var proThree = new Promise(resolve=> setTimeout(()=> resolve(3),1000));
Promise.all([proOne,proTwo,proThree]).catch(alert);
</script>
</html>
最後會彈出2。
Promise的語法如下:
// iterable 代表可迭代物件
let promise = Promise.all(iterable);
iterable 可以出現非Promise型別的值如下圖所示:
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
}),
2,
3
]).then(alert); // 1, 2, 3
Promise.allSettled
Promise.all中任意一個promise的失敗,會導致最終的任務失敗,但是有的時候我們希望看到所有任務的執行結果,有三個任務A、B、C執行,即使A失敗了,那麼B和C的結果我們也是關注的,由此就引出了Promise.allSettled:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
// 這三個地址用於獲取使用者資訊。
// 即使一個失敗了我們仍然對其他的感興趣
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://no-such-url'
];
Promise.allSettled(urls.map(url => fetch(url)))
.then(results => { // (*)
results.forEach((result, num) => {
if (result.status == "fulfilled") {
alert(`${urls[num]}: ${result.value.status}`);
}
if (result.status == "rejected") {
alert(`${urls[num]}: ${result.reason}`);
}
});
});
</script>
</html>
注意Promise.allSettled是新新增的特性,舊的瀏覽器可能不支援,那就需要我們包裝一下,更為專業的說法是polyfill:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
if(!Promise.allSettled){
const rejectHandler = reason => ({status:'rejected',reason});
const resolveHandler = value => ({status:'fulfilled',value});
Promise.allSettled = function(promises){
const convertedPromises = promises.map(p => Promise.resolve(p)).then(resolveHandler,rejectHandler);
Promise.all(convertedPromises);
}
}
</script>
</html>
promises透過map中的Promise.resolve(p)將輸入值轉換為Promise,然後對每個Promise都新增.then處理程式。這樣我們就可以使用Promise.allSettled來獲取所有給定的promise的結果,即使其中一些被reject。
Promise.race
有的時候我們也只關注第一名,那該怎麼做呢? 由此引出Promise.race:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
Promise.race([
new Promise((resolve,reject) => setTimeout(()=> resolve(1),1000)),
new Promise((resolve,reject) => setTimeout(()=> resolve(2),2000)),
new Promise((resolve,reject) => setTimeout(()=> resolve(3),2000))
]).then(alert);
</script>
</html>
這裡第一個Promise最快,所以alert(1)。
Promise.any
但最快有可能是犯規的對嗎? 即這個任務reject,有的時候我們只想關注沒有違規的第一名,由此引出Promise.any:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
Promise.race([
new Promise((resolve,reject) => setTimeout(()=> reject(1),1000)),
new Promise((resolve,reject) => setTimeout(()=> resolve(2),2000)),
new Promise((resolve,reject) => setTimeout(()=> resolve(3),2000))
]).then(alert);
</script>
</html>
雖然第一個promise最快,但是它犯規了,所以alert的是2。那要是全犯規了呢, 那就需要catch一下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=S, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
Promise.any([
new Promise((resolve,reject) => setTimeout(()=> reject("犯規1"),1000)),
new Promise((resolve,reject) => setTimeout(()=> reject("犯規2"),2000))
]).catch(error =>{
console.log(error.constructor.name); // AggregateError
console.log(error.errors[0]); // 控制檯輸出犯規1
console.log(error.errors[1]); // 控制檯輸出犯規2
} )
</script>
</html>
async/await 簡介
有的時候非同步方法可能比較龐大,直接用Promise寫可能不大雅觀,由此我們就引出async/await, 我們只用在需要用到Promise的非同步函式上寫上async這個關鍵字,這個函式的返回值將會自動的被包裝進Promise:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
async function f() {
return 1;
}
f().then(alert); // 1
</script>
</html>
但是我們常常會碰到這樣一種情況, 我們封裝了一個函式,這個函式上有async,但是內部的實現有兩個操作A、B,B操作需要等到A完成之後再執行,由此就引出了await:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=
, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
async function demo(){
let responseOne = fetch('http://localhost:8080/hello');
console.log("1");
let json = await responseOne.json; // 語句一
let response = fetch('http://localhost:8080/hello');
let jsonTwo = await responseOne.json; // 語句二
console.log("2");
}
demo();
</script>
</html>
這裡用async/wait改寫了我們開頭的例子,JavaScript引擎執行到語句一的時候會等待fetch操作完成,才執行await後的程式碼。這一看好像清爽了許多。注意await不能在沒有async的函式中執行。相比較於Promise.then,它只是獲取promise結果的一個更加優雅的寫法,並且看起來更易於讀寫。那如果等待的promise發生了異常呢,該怎麼進行錯誤處理呢?有兩種選擇,一種是try catch,另一種是用非同步函式返回的promise產生的catch來處理這個錯誤:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=
, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
async function demo01(){
try{
let response = await fetch('/no-user-here');
let user = await response.json();
}catch(error){
alert(error);
}
}
demo01();
async function demo02(){
let response = await fetch('/no-user-here');
let user = await response.json();
}
demo02().catch(alert);
</script>
</html>
總結一下
JavaScript也有非同步任務,如果我們想協調非同步任務,就可以選擇使用Promise,當前的我們只有一個非同步任務,我們希望在非同步任務回撥,那麼可以在用Promise.then,如果你想關注錯誤結果,那麼可以用catch,如果你想在任務完成之後做一些清理工作,那麼可以用Promise的finally。現在我們將非同步任務的數目提升,提升到三個,如果我們想再這三個任務完成之後觸發一些操作,那麼我們可以用Promise.all,但是但Promise.all的缺陷在於,一個任務失敗之後,我們看不到成功任務的結果,如果任務成功與失敗的結果,那麼就可以用Promise.allSettled。但有的時候我們也指向關注“第一名”,那就用Promise.race,但有的時候我們也只想要沒犯規的第一名,這也就是Promise.any。有的時候我們也不想用then回撥的這種方式,這寫起來可能有點煩,那就可以用async/await 。
參考資料
- 《現代JavaScript教程》 https://zh.javascript.info/as...