篇頭
我們工作或面試中,經(jīng)常遇到這些問題:如何更編寫更優(yōu)美的異步代碼?你了解promise, async/await嗎?知道他們的執(zhí)行順序嗎?那么接下來我們將從淺入深逐步吃透這些問題。
引子
眾所周知 Javascript 是采用的單線程的工作模式?那么為什么會用這種模式呢?
很重要的一點是因為我們頁面交互的合適是操作DOM,為避免多線程可能會產(chǎn)生的線程同步問題,因為采用的是單線程工作模式。
- 優(yōu)點:程序執(zhí)行更安全、更簡單
- 缺點:容易被耗時長的任務(wù)卡住,造成程序假死
在這種單線程的工作模式下,我們?yōu)榻鉀Q以上問題就出現(xiàn)了:同步模式、異步模式
同步模式
大多數(shù)任務(wù)都會異常同步模式執(zhí)行,任務(wù)會一次進(jìn)入任務(wù)的調(diào)用棧依次執(zhí)行,執(zhí)行后推出調(diào)用棧,這里我們不過多展開。
異步模式
一般多是耗時長的任務(wù),開啟后就立即執(zhí)行下個任務(wù),而自己本身的任務(wù)是已回調(diào)函數(shù)的方式去處理后續(xù)邏輯。這是我們解決單線程模式缺點的關(guān)鍵。
Promise
Promise 對象代表一個異步操作,有三種狀態(tài):
- pending: 初始狀態(tài),不是成功或失敗狀態(tài)。
- fulfilled: 意味著操作成功完成。
- rejected: 意味著操作失敗。
const promise = new Promise((resolve,reject) => {
rej(new Error("promise rejected"))
})
promise.then(val => {
console.log("resolve", val);
}).catch(err => {
console.log("reject", err);
})
console.log("end")
// end
// eject Error: promise rejected
then方法的參數(shù)是函數(shù),如果不是函數(shù)可以直接無視
模擬Ajax
function ajax(url) {
return new Promise((resolve,reject) => {
let xhr = new XMLHttpRequest()
xhr.open("get", url)
xhr.responseType = "json"
xhr.onload = function() {
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
ajax("package.json").then(res => {
console.log(res)
})
由此我們可以看出promise的本質(zhì)就是已回調(diào)函數(shù)的方式去定義異步任務(wù)結(jié)束之后需要執(zhí)行的任務(wù)
但是好多剛開始學(xué)習(xí)用promise的同學(xué)經(jīng)常會陷入一個誤區(qū),一個異步任務(wù)在另一個異步任務(wù)回調(diào)結(jié)束后執(zhí)行仍然使用了嵌套,這就背離了我們使用promise的初衷。那么解決這個問題的方式就是利用promise的鏈?zhǔn)秸{(diào)用
Promise的鏈?zhǔn)秸{(diào)用
const promise = new Promise((resolve,reject) => {
resolve()
})
promise.then(() => {
return 1;
}).then((res) => {
console.log(res);
// 輸出1
}).then(() => {
console.log("3");
})
-
Promise的then方法會返回一個全新的Promise對象 - 后面的
then都是在為上一個的then返回的promise添加狀態(tài)明確過后的回調(diào) - 前面的
then方法中回調(diào)函數(shù)的返回值會作為后面then方法回調(diào)的參數(shù) - 如果回調(diào)中返回的是
Promise, 那么后面的then方法的回調(diào)會等待它的結(jié)束
Promise的異常處理
常見的異常處理
//第一種
promise.then(resolve => {
console.log("resolve", resolve);
}, reject => {
console.log("reject", reject);
})
//第二種
promise.then(resolve => {
console.log("resolve", resolve);
}).catch(err => {
console.log("reject", err);
})
是掛載在全局上,用來處理代碼中沒有被手動捕獲的異常,雖說提供了該方法,但還是建議全手動捕獲可能出現(xiàn)的異常
瀏覽器中的使用方式:
window.addEventListener("unhandledrejection", event => {
const {reason, promise} = event;
//reason => Promise 失敗的原因, 一般是個對象
//promise => 出現(xiàn)異常的 Primose對象
event.preventDefault()
},false)
node環(huán)境下的使用方式:
process.on("unhandledRejection", (reason, promise) => {
//reason => Promise 失敗的原因, 一般是個對象
//promise => 出現(xiàn)異常的 Primose對象
})
Promise的靜態(tài)方法
resolve
Promise.resolve("1")
.then(val => {console.log(val)})
new Promise((resolve,reject) => {
resolve("1")
})
//兩者等價
let p1 = new Promise((resolve,reject) => {
resolve("1")
})
let p2 = Promise.resolve(p1)
console.log(p1 === p2)
// true
由此可見將一個Promise對象包裹在另一個Promise對象里面返回的還是最開始的Promise
初此之外還一個特殊的點,如果Promise的參數(shù)是一個包含著如下的一個then的對象,那么也可以當(dāng)做一個回調(diào)去執(zhí)行
Promise.resolve({
then: (onFulfilled,onReject) => {
onFulfilled("test 1")
}
}).then(val => {
console.log(val)
})
// test 1
reject
同理
Promise.reject("err").catch(err => console.log(err))
Promise 并行執(zhí)行
我們常見的場景是一個頁面加載時需要同時請求多個接口,而接口之間又不相互關(guān)聯(lián),假如依次串行請求明顯時間會很長,那么我們需要將其并行執(zhí)行。
那么如何知道并行的請求都已完成返回的時間點呢?那么需要我們利用到Promise.all()
Promise.all
Promise.all([promise1, promise2])
.then(res => {
//res 是一個返回值的數(shù)組
})
.catch(err => {
//只要有一個promise 異常
})
Promise.race
Promise.all([promise1, promise2])
.then(res => {
//等待第一個promise結(jié)束
})
Promise的執(zhí)行時序 (宏任務(wù)、微任務(wù))
宏任務(wù)
大部分異步調(diào)用都是宏任務(wù)
微任務(wù)
常見微任務(wù): Promise & MutationObserver & process.nextTick(node環(huán)境下)
Generator 異步方案
Generator(生成器函數(shù))是ES2015被提出來的, 下面我們先回顧一下
function * foo() {
console.log("start")
try{
let val = yield "foo"
console.log(val)
} catch(e) {
console.log(e)
}
}
const generator = foo() //沒有立即執(zhí)行
const result1 = generator.next() //會執(zhí)行foo() 直到遇到關(guān)鍵字 yield為止
console.log(result1)
//start
//{value: "foo", done: false}
const result2 = generator.next("bar") //剛才yield繼續(xù)往下執(zhí)行, next里的參數(shù)可以復(fù)制給 yield前邊的變量
console.log(result2)
// bar
generator.throw(new Error("generator error"))
那么接下來我們可以利用Generator中這一特性結(jié)合Promise使用
function * main() {
const json = yield ajax("package.json") //那么我們這里就近似一種同步的書寫方式
console.log(json)
}
const g = main();
const result = g.next();
result.value.then(data => {
g.next(data)
})
為避免多個promise造成代碼的堆積,我們可以利用一下遞歸, 并且再做一下封裝
function * main() {
const json1 = yield ajax("package.json")
console.log(json1)
const json2 = yield ajax("package.json")
console.log(json2)
const json3 = yield ajax("package.json")
console.log(json3)
}
function co(generator) {
const g = main();
function loopHandle(result) {
if (result.done) return
result.value
.then(data => loopHandle(g.next(data)), err => g.throw(err))
}
loopHandle(g.next())
}
co(main)
Aysnc/Await
aysnc/await是generator的語法糖
async function main() {
const json1 = await ajax("package.json")
console.log(json1)
const json2 = await ajax("package.json")
console.log(json2)
const json3 = await ajax("package.json")
console.log(json3)
}
main()