JavaScript Promise對象
- 2020 年 8 月 14 日
- 筆記
- javascript
事件循環
JavaScript
是一門單線程的編程語言,所以沒有並發並行等特性。
為了協調事件、用戶交互、腳本、UI 渲染和網絡處理等行為,防止主線程的不阻塞,(事件循環)Event Loop
的方案應用而生。
JavaScript
處理任務是在等待任務、執行任務 、休眠等待新任務中不斷循環中,也稱這種機製為事件循環。
主線程中的任務執行完後,才執行任務隊列中的任務
有新任務到來時會將其放入隊列,採取先進先執行的策略執行隊列中的任務
比如多個
setTimeout
同時到時間了,就要依次執行
任務包括 script
(整體代碼)、 setTimeout
、setInterval
、DOM渲染、DOM事件、Promise
、XMLHTTPREQUEST
等
任務詳解
任務分類
任務大致分為以下三種:
主線程任務
應放入宏隊列中的任務
應放入微隊列中的任務
放入宏隊列中的任務 | ||
---|---|---|
# | 瀏覽器 | Node |
setTimeout | √ | √ |
setInterval | √ | √ |
setImmediate | x | √ |
requestAnimationFrame | √ | x |
放入微隊列中的任務 | ||
---|---|---|
# | 瀏覽器 | Node |
process.nextTick | x | √ |
MutationObserver | √ | x |
Promise.then catch finally | √ | √ |
執行順序
根據任務的不同,執行順序也有所不同:
1.主線程任務
2.微隊列任務
3.宏隊列任務
<script>
"use strict";
new Promise(resolve => {
console.log("主線程任務執行 1...")
resolve();
}).then(_ => {
console.log("微隊列任務執行 7...");
});
console.log("主線程任務執行 2...");
setTimeout(() => {
console.log("宏隊列任務執行 9...");
}, 1);
console.log("主線程任務執行 3...");
new Promise(resolve => {
console.log("主線程任務執行 4...")
resolve();
}).then(_ => {
console.log("微隊列任務執行 8...");
});
console.log("主線程任務執行 5...");
console.log("主線程任務執行 6...");
/*
主線程任務執行 1...
主線程任務執行 2...
主線程任務執行 3...
主線程任務執行 4...
主線程任務執行 5...
主線程任務執行 6...
微隊列任務執行 7...
微隊列任務執行 8...
宏隊列任務執行 9...
*/
</script>
作用體現
使用Promise
能讓代碼變得更易閱讀,方便後期維護。
特別是在回調函數嵌套上,更應該使用Promise
來書寫代碼。
嵌套問題
以下示例將展示通過Js
來使得<div>
標籤形態在不同時刻發生變化。
代碼邏輯雖然清晰但是定時器回調函數嵌套太過複雜,閱讀體驗較差。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
width: 100px;
height: 100px;
background-color: red;
transition: 1s;
}
button {
margin-top: 20px;
}
</style>
</head>
<body>
<div></div>
<button>點我</button>
</body>
<script>
"use strict";
document.querySelector("button").addEventListener("click", () => {
let div = document.querySelector("div");
div.style.backgroundColor = "blue";
setTimeout(() => {
div.style.width = "50px";
setTimeout(() => {
div.style.transform = "translate(100px)";
setTimeout(() => {
div.style.width = "100px";
div.style.backgroundColor = "red";
setTimeout(() => {
div.style.backgroundColor = "yellow";
},1000);
}, 1000);
}, 1000);
}, 1000);
});
</script>
</html>
嘗試解決
使用Promise
來解決該問題。
這裡看不懂沒關係,下面會慢慢進行剖析,只是感受一下是不是嵌套沒那麼嚴重了看起來好看多了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
width: 100px;
height: 100px;
background-color: red;
transition: 1s;
}
button {
margin-top: 20px;
}
</style>
</head>
<body>
<div></div>
<button>點我</button>
</body>
<script>
"use strict";
function chain(callback, time=1000) {
return new Promise(function (resolve, reject) {
setTimeout(() => {
let res = callback();
resolve(res);
}, time);
});
}
document.querySelector("button").addEventListener("click", () => {
new Promise(function (resolve, reject) {
let div = document.querySelector("div");
div.style.backgroundColor = "blue";
resolve(div);
}).then(div => {
return chain(() => {
div.style.width = "50px";
return div;
});
}).then(div => {
return chain(() => {
div.style.transform = "translate(100px)";
return div;
});
}).then(div => {
return chain(() => {
div.style.width = "100px";
div.style.backgroundColor = "red";
return div;
})
}).then(div => {
return chain(() => {
div.style.backgroundColor = "yellow";
return div;
})
})
});
</script>
Promise
JavaScript
中存在很多異步操作,Promise
將異步操作隊列化,按照期望的順序執行,返回符合預期的結果。
可以通過鏈式調用多個 Promise
達到我們的目的,如同上面示例一樣會讓代碼可讀性大幅度提升。
聲明狀態
每一個Promise
對象都接收一個函數,該函數需要提供兩個參數,分別是resolve
以及reject
,代表當前函數中的任務成功與失敗,這是屬於線程任務的,所以會優先執行。
此外,每一個Promise
對象都具有三種狀態,分別是pending
,fulfilled
,rejected
。
當一個Promise
對象狀態改變過後,將不能再次改變。
pending
指初始等待狀態,初始化promise
時的狀態
resolve
指已經解決,將promise
狀態設置為fulfilled
reject
指拒絕處理或未解決,將promise
狀態設置為rejected
當沒有使用 resolve
或 reject
更改狀態時,狀態為 pending
<script>
"use strict";
let p1 = new Promise(function (resolve, reject) { });
console.log(p1); // Promise {<pending>}
</script>
使用resolve
修改狀態後,狀態為fulfilled
<script>
"use strict";
let p1 = new Promise(function (resolve, reject) {
resolve("已解決");
});
console.log(p1); // Promise {<fulfilled>: "已解決"}
</script>
使用reject
修改狀態後,狀態為rejected
<script>
"use strict";
let p1 = new Promise(function (resolve, reject) {
reject("未解決");
});
console.log(p1); // Promise {<rejected>: "未解決"}
</script>
then
在一個Promise
對象狀態為resolve
或reject
時,可以緊跟then
方法,該方法可接收兩個個函數對象,用於處理Promise
對象reject
或resolve
傳遞過來的值。
<script>
"use strict";
new Promise(function (resolve, reject) {
reject("未解決");
})
.then(success => {
console.log("resolve:", success);
},
error => {
console.log("reject:", error); // resolve: 未解決
}
);
</script>
catch
每個then
都可以指定第二個函數用於處理上一個Promise
失敗的情況,如果每個then
都進行這樣設置會顯得很麻煩,所以我們只需要使用catch
即可。
catch
可以捕獲之前所有 promise
的錯誤,所以建議將 catch
放在最後。
建議使用
catch
處理錯誤將
catch
放在最後面用於統一處理前面發生的錯誤
錯誤是冒泡操作的,下面沒有任何一個then
定義第二個函數,將一直冒泡到 catch
處理錯誤
<script>
"use strict";
new Promise((resolve, reject) => {
reject("失敗");
}).then(success => {
console.log("成功");
}).then(success => {
console.log("成功");
}).catch(error => {
console.log(error); // 失敗
})
</script>
catch
也可捕捉到throw
自動觸發的異常。
<script>
"use strict";
new Promise((resolve, reject) => {
resolve("成功");
}).then(success => {
throw new Error("失敗");
}).catch(error=>{
console.log(error); // Error: 失敗
})
</script>
finally
無論狀態是resolve
或 reject
都會執行此動作,finally
與狀態無關。
<script>
"use strict";
new Promise((resolve, reject) => {
reject("失敗");
}).then(success => {
console.log("成功");
}).catch(error => {
console.log(error); // 失敗
}).finally(() => {
console.log("都會執行"); // 都會執行
})
</script>
鏈式調用
使用Promise
進行鏈式調用,可以規避掉嵌套問題。
基本概念
其實每一個then
都是一個新的Promise
,默認返回為fulfilled
狀態。
<script>
"use strict";
let p1 = new Promise(function (resolve, reject) {
resolve("已解決");
})
let p2 = p1.then(success => {
console.log(success);
}, error => {
console.log(error);
});
setTimeout(() => {
console.log(p2); // 宏任務隊列中的任務最後執行 Promise {<fulfilled>: undefined}
},3000)
</script>
此時就會產生一種鏈式關係,每一個then
都是一個新的Promise
對象,而每個then
的作用又都是處理上個Promise
對象的狀態。
要想使用鏈式調用,一定要搞明白每一個then
的返回值。
返回了一個值,那麼
then
返回的Promise
將會成為接受狀態,並且將返回的值作為接受狀態的回調函數的參數值。沒有返回任何值,那麼
then
返回的Promise
將會成為接受狀態,並且該接受狀態的回調函數的參數值為undefined
。拋出一個錯誤,那麼
then
返回的Promise
將會成為拒絕狀態,並且將拋出的錯誤作為拒絕狀態的回調函數的參數值。返回一個已經是接受狀態的
Promise
,那麼then
返回的Promise
也會成為接受狀態,並且將那個Promise
的接受狀態的回調函數的參數值作為該被返回的Promise
的接受狀態回調函數的參數值。返回一個已經是拒絕狀態的
Promise
,那麼then
返回的Promise
也會成為拒絕狀態,並且將那個Promise
的拒絕狀態的回調函數的參數值作為該被返回的Promise
的拒絕狀態回調函數的參數值。返回一個未定狀態(
pending
)的Promise
,那麼then
返回 Promise 的狀態也是未定的,並且它的終態與那個 Promise 的終態相同;同時,它變為終態時調用的回調函數參數與那個 Promise 變為終態時的回調函數的參數是相同的。
無返回
上一個then
無返回值時該then
創建的Promise
對象為resolve
狀態。
下一個then
會立即執行,接收值為undefined
。
<script>
"use strict";
new Promise((resolve, reject) => {
resolve("成功");
}).then(success => {
console.log("無返回1"); // 上一個Promise狀態是resolve 立刻執行
}).then(success => {
console.log("無返回2"); // 上一個Promise狀態是resolve 立刻執行
})
</script>
返回值
上一個then
有返回值時該then
創建的Promise
對象為resolve
狀態。
下一個then
會立即執行,接收值為上一個then
的返回值。
<script>
"use strict";
new Promise((resolve, reject) => {
resolve("成功");
}).then(success => {
return "v1" // 上一個Promise狀態是resolve 立刻執行
}).then(success => {
console.log(success); // v1 上一個Promise狀態是resolve 立刻執行
})
</script>
返回Promise
上一個then
有返回值且該返回值是一個Promise
對象的話下一個then
會等待該Promise
對象狀態改變後再進行執行,接收值根據被返回的Promise
對象的任務處理狀態來決定。
<script>
"use strict";
new Promise((resolve, reject) => {
resolve("成功");
}).then(success => {
return new Promise((resolve, reject) => {
// resolve("成功");
})
}).then(success => {
console.log(success); // 上一個Promise狀態是pending 不執行,等待狀態變化
})
</script>
嵌套解決
我們可以利用在一個then
中返回Promise
下面的then
會等待狀態的特性,對定時器回調函數嵌套進行優化。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
width: 100px;
height: 100px;
background-color: red;
transition: 1s;
}
button {
margin-top: 20px;
}
</style>
</head>
<body>
<div></div>
<button>點我</button>
</body>
<script>
"use strict";
document.querySelector("button").addEventListener("click", () => {
new Promise(function (resolve, reject) {
let div = document.querySelector("div");
div.style.backgroundColor = "blue";
resolve(div);
}).then(div => {
return new Promise(function (resolve, reject) {
setTimeout(() => {
div.style.width = "50px";
resolve(div);
}, 1000);
})
}).then(div => {
return new Promise(function (resolve, reject) {
setTimeout(() => {
div.style.transform = "translate(100px)";
resolve(div);
}, 1000);
})
}).then(div => {
return new Promise(function (resolve, reject) {
setTimeout(() => {
div.style.width = "100px";
div.style.backgroundColor = "red";
resolve(div);
}, 1000);
})
}).then(div => {
return new Promise(function (resolve, reject) {
setTimeout(() => {
div.style.backgroundColor = "yellow";
resolve(div);
}, 1000);
})
})
});
</script>
代碼優化
繼續對上面的代碼做優化。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
width: 100px;
height: 100px;
background-color: red;
transition: 1s;
}
button {
margin-top: 20px;
}
</style>
</head>
<body>
<div></div>
<button>點我</button>
</body>
<script>
"use strict";
function chain(callback, time=1000) {
return new Promise(function (resolve, reject) {
setTimeout(() => {
let res = callback();
resolve(res);
}, time);
});
}
document.querySelector("button").addEventListener("click", () => {
new Promise(function (resolve, reject) {
let div = document.querySelector("div");
div.style.backgroundColor = "blue";
resolve(div);
}).then(div => {
return chain(() => {
div.style.width = "50px";
return div;
});
}).then(div => {
return chain(() => {
div.style.transform = "translate(100px)";
return div;
});
}).then(div => {
return chain(() => {
div.style.width = "100px";
div.style.backgroundColor = "red";
return div;
})
}).then(div => {
return chain(() => {
div.style.width = "100px";
div.style.backgroundColor = "yellow";
return div;
})
})
});
</script>
擴展接口
resolve
使用 Promise.resolve()
方法可以快速的返回一個狀態是resolve
的Promise
對象。
<script>
"use strict";
Promise.resolve("成功").then(success=>console.log(success)); // 成功
</script>
reject
使用 Promise.reject()
方法可以快速的返回一個狀態是reject
的Promise
對象。
<script>
"use strict";
Promise.reject("失敗").then(null,error=>console.log(error)); // 失敗
// 使用null來對成功的處理進行佔位
</script>
all
使用Promise.all()
方法可以同時執行多個並行異步操作,比如頁面加載時同進獲取課程列表與推薦課程。
任何一個
Promise
執行失敗就會調用catch
方法適用於一次發送多個異步操作
參數必須是可迭代類型,如
Array/Set
成功後返回
Promise
結果的有序數組
以下示例將展示同時提交兩個異步操作,只有當全部成功時才會執行Promise.all()
其下的then
<script>
"use strict";
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("成功");
}, 3000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("成功");
}, 3000);
});
Promise.all([p1, p2])
.then(success => {
console.log(success); // (2) ["成功", "成功"]
})
.catch(error => {
console.log(error); // 任何一個失敗都會執行這裡
});
</script>
allSettled
allSettled
用於處理多個Promise
,只關注執行完成,不關注是否全部執行成功,allSettled
狀態只會是fulfilled
。
<script>
"use strict";
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("成功");
}, 1000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("成功");
}, 3000);
});
Promise.allSettled([p1, p2])
.then(success => {
console.log(success);
})
/*
[{status: "fulfilled", value: "成功"}, {status: "fulfilled", value: "成功"}]
*/
</script>
race
使用Promise.race()
處理容錯異步,和race
單詞一樣哪個Promise
快用哪個,哪個先返回用哪個。
其實這在某些資源引用上比較常用,可以添加多個資源地址進行請求,誰先快就用誰的。
以最快返回的
Promise
為準如果最快返加的狀態為
rejected
那整個Promise
為rejected
執行cache
如果參數不是
Promise
,內部將自動轉為Promise
下面示例中成功1比較快,就用成功1的。
<script>
"use strict";
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("成功1");
}, 1000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("成功2");
}, 3000);
});
Promise.race([p1, p2])
.then(success => {
console.log(success); // 成功1
})
</script>
async/await
使用 async/await
是Promise
的語法糖,可以讓編寫 Promise
更清晰易懂,也是推薦編寫Promise
的方式。
async/await
本質還是Promise
,只是更簡潔的語法糖書寫
async
在某一個函數前加上async
,該函數會返回一個Promise
對象。
我們可以依照標準Promise
來操縱該對象。
<script>
"use strict";
async function get() {
return "請求成功...";
}
get().then(success => {
console.log(success); // 請求成功...
})
</script>
await
使用 await
關鍵詞後會等待Promise
完。
await
後面一般是Promise
,如果不是直接返回
await
必須放在async
定義的函數中使用
await
用於替代then
使編碼更優雅
<script>
"use strict";
async function get() {
const ajax = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve("返回的結果");
},3000);
});
let result = await ajax;
console.log(result); // 返回的結果
}
get();
</script>
一般await
後面是外部其它的Promise
對象
<script>
"use strict";
async function getName() {
return new Promise((resolve, reject) => {
resolve("姓名數據...");
});
}
async function getGrades() {
return new Promise((resolve, reject) => {
resolve("成績數據...");
});
}
async function run() {
let nameSet = await getName();
let gradesSet = await getGrades();
console.log(nameSet);
console.log(gradesSet);
}
run();
</script>
異常處理
Promise
狀態為rejected
其實我們就可以將它歸為出現異常了。
當一個await
發生異常時,其他的await
不會進行執行。
<script>
"use strict";
async function getName() {
return new Promise((resolve, reject) => {
reject("姓名數據獲取失敗...");
})
}
async function getGrades() {
return new Promise((resolve, reject) => {
resolve("成績數據...");
});
}
async function run() {
let nameSet = await getName(); // Uncaught (in promise) 姓名數據獲取失敗...
let gradesSet = await getGrades(); // 不執行
}
run();
</script>
如果在async
中不確定會不會拋出異常,我們可以在接收時使用catch
進行處理。
<script>
"use strict";
async function getName() {
return new Promise((resolve, reject) => {
reject("姓名數據獲取失敗...");
})
}
async function run() {
let nameSet = await getName().catch(error => console.log(error));
}
run();
</script>
更推薦寫成下面這種形式
<script>
"use strict";
async function getName() {
return new Promise((resolve, reject) => {
reject("姓名數據獲取失敗...");
})
.catch(error => console.log(error));
}
async function run() {
let nameSet = await getName();
}
run();
</script>
也可使用try...catch
進行處理。
<script>
"use strict";
async function getName() {
return new Promise((resolve, reject) => {
reject("姓名數據獲取失敗...");
});
}
async function run() {
try {
let nameSet = await getName();
} catch (e) {
console.log(e); // 姓名數據獲取失敗...
}
}
run();
</script>