在異步編程中,Promise 扮演了舉足輕重的角色,比傳統的解決方案(回調函數和事件)更合理和更強大。可能有些小夥伴會有這樣的疑問:2020年了,怎麼還在談論Promise?事實上,有些朋友對于這個幾乎每天都在打交道的“老朋友”,貌似全懂,但稍加深入就可能疑問百出,本文帶大家深入理解這個熟悉的陌生人—— Promise.
new Promise( function(resolve, reject) {...} /* executor */ )
值得注意的是, Promise 是用來管理異步編程的,它本身不是異步的 ,new Promise的時候會立即把executor函數執行,隻不過我們一般會在executor函數中處理一個異步操作。比如下面代碼中,一開始是會先打印出2。
let p1 = new Promise(()=>{
setTimeout(()=>{
console.log(1)
},1000)
console.log(2)
})
console.log(3) // 2 3 1
Promise 采用了回調函數延遲綁定技術,在執行 resolve 函數的時候,回調函數還沒有綁定,那麼隻能 推遲回調函數的執行 。這具體是啥意思呢?我們先來看下面的例子:
let p1 = new Promise((resolve,reject)=>{
console.log(1);
resolve('浪裡行舟')
console.log(2)
})
// then:設置成功或者失敗後處理的方法
p1.then(result=>{
//p1延遲綁定回調函數
console.log('成功 ' result)
},reason=>{
console.log('失敗 ' reason)
})
console.log(3)
// 1
// 2
// 3
// 成功 浪裡行舟
new Promise的時候先執行executor函數,打印出 1、2,Promise在執行resolve時,觸發微任務,還是繼續往下執行同步任務, 執行p1.then時,存儲起來兩個函數(此時這兩個函數還沒有執行),然後打印出3,此時同步任務執行完成,最後執行剛剛那個微任務,從而執行.then中成功的方法。
Promise 對象的錯誤 具有“冒泡”性質,會一直向後傳遞 ,直到被 onReject 函數處理或 catch 語句捕獲為止。具備了這樣“冒泡”的特性後,就不需要在每個 Promise 對象中單獨捕獲異常了。
要遇到一個then,要執行成功或者失敗的方法,但如果此方法并沒有在當前then中被定義,則順延到下一個對應的函數
function executor (resolve, reject) {
let rand = Math.random()
console.log(1)
console.log(rand)
if (rand > 0.5) {
resolve()
} else {
reject()
}
}
var p0 = new Promise(executor)
var p1 = p0.then((value) => {
console.log('succeed-1')
return new Promise(executor)
})
var p2 = p1.then((value) => {
console.log('succeed-2')
return new Promise(executor)
})
p2.catch((error) => {
console.log('error', error)
})
console.log(2)
這段代碼有三個 Promise 對象:p0~p2。無論哪個對象裡面抛出異常,都可以通過最後一個對象 p2.catch 來捕獲異常,通過這種方式可以将所有 Promise 對象的錯誤合并到一個函數來處理,這樣就解決了每個任務都需要單獨處理異常的問題。
通過這種方式,我們就消滅了嵌套調用和頻繁的錯誤處理,這樣使得我們寫出來的代碼更加優雅,更加符合人的線性思維。
我們都知道可以把多個Promise連接到一起來表示一系列異步驟。這種方式可以實現的關鍵在于以下兩個Promise 固有行為特性:
先通過下面的例子,來解釋一下剛剛這段話是什麼意思,然後詳細介紹下鍊式調用的執行流程
let p1=new Promise((resolve,reject)=>{
resolve(100) // 決定了下個then中成功方法會被執行
})
// 連接p1
let p2=p1.then(result=>{
console.log('成功1 ' result)
return Promise.reject(1)
// 返回一個新的Promise實例,決定了當前實例是失敗的,所以決定下一個then中失敗方法會被執行
},reason=>{
console.log('失敗1 ' reason)
return 200
})
// 連接p2
let p3=p2.then(result=>{
console.log('成功2 ' result)
},reason=>{
console.log('失敗2 ' reason)
})
// 成功1 100
// 失敗2 1
我們通過返回 Promise.reject(1) ,完成了第一個調用then創建并返回的promise p2。p2的then調用在運行時會從return Promise.reject(1) 語句接受完成值。當然,p2.then又創建了另一個新的promise,可以用變量p3存儲。
new Promise出來的實例,成功或者失敗,取決于executor函數執行的時候, 執行的是resolve還是reject決定的 ,或 executor函數執行發生異常錯誤 ,這兩種情況都會把實例狀态改為失敗的。
p2執行then返回的新實例的狀态,決定下一個then中哪一個方法會被執行,有以下幾種情況:
我們再來看個例子
new Promise(resolve=>{
resolve(a) // 報錯
// 這個executor函數執行發生異常錯誤,決定下個then失敗方法會被執行
}).then(result=>{
console.log(`成功:${result}`)
return result*10
},reason=>{
console.log(`失敗:${reason}`)
// 執行這句時候,沒有發生異常或者返回一個失敗的Promise實例,所以下個then成功方法會被執行
// 這裡沒有return,最後會返回 undefined
}).then(result=>{
console.log(`成功:${result}`)
},reason=>{
console.log(`失敗:${reason}`)
})
// 失敗:ReferenceError: a is not defined
// 成功:undefined
從上面一些例子,我們可以看出,雖然使用 Promise 能很好地解決回調地獄的問題,但是這種方式充滿了 Promise 的 then() 方法,如果處理流程比較複雜的話,那麼整段代碼将充斥着 then,語義化不明顯,代碼不能很好地表示執行流程。
ES7中新增的異步編程方法,async/await的實現是基于 Promise的,簡單而言就是async 函數就是返回Promise對象,是generator的語法糖。很多人認為async/await是異步操作的終極解決方案:
不過也存在一些缺點,因為 await 将異步代碼改造成了同步代碼,如果多個異步代碼沒有依賴性卻使用了 await 會導緻性能上的降低。
async function test() {
// 以下代碼沒有依賴性的話,完全可以使用 Promise.all 的方式
// 如果有依賴性的話,其實就是解決回調地獄的例子了
await fetch(url1)
await fetch(url2)
await fetch(url3)
}
觀察下面這段代碼,你能判斷出打印出來的内容是什麼嗎?
let p1 = Promise.resolve(1)
let p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2)
}, 1000)
})
async function fn() {
console.log(1)
// 當代碼執行到此行(先把此行),構建一個異步的微任務
// 等待promise返回結果,并且await下面的代碼也都被列到任務隊列中
let result1 = await p2
console.log(3)
let result2 = await p1
console.log(4)
}
fn()
console.log(2)
// 1 2 3 4
如果 await 右側表達邏輯是個 promise,await會等待這個promise的返回結果, 隻有返回的狀态是resolved情況 ,才會把結果返回,如果promise是失敗狀态,則await不會接收其返回結果,await下面的代碼也不會在繼續執行。
let p1 = Promise.reject(100)
async function fn1() {
let result = await p1
console.log(1) //這行代碼不會執行
}
我們再來看道比較複雜的題目:
console.log(1)
setTimeout(()=>{console.log(2)},1000)
async function fn(){
console.log(3)
setTimeout(()=>{console.log(4)},20)
return Promise.reject()
}
async function run(){
console.log(5)
await fn()
console.log(6)
}
run()
//需要執行150ms左右
for(let i=0;i<90000000;i ){}
setTimeout(()=>{
console.log(7)
new Promise(resolve=>{
console.log(8)
resolve()
}).then(()=>{console.log(9)})
},0)
console.log(10)
// 1 5 3 10 4 7 8 9 2
做這道題之前,讀者需明白:
接下來,我們一步一步分析:
Promise.resolve(value)方法返回一個以給定值解析後的Promise 對象。 Promise.resolve()等價于下面的寫法:
Promise.resolve('foo')
// 等價于
new Promise(resolve => resolve('foo'))
Promise.resolve方法的參數分成四種情況。
(1)參數是一個 Promise 實例
如果參數是 Promise 實例,那麼Promise.resolve将 不做任何修改、原封不動地 返回這個實例。
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
上面代碼中,p1是一個 Promise,3 秒之後變為rejected。p2的狀态在 1 秒之後改變,resolve方法返回的是p1。由于p2返回的是另一個 Promise,導緻p2自己的狀态無效了,由p1的狀态決定p2的狀态。所以,後面的then語句都變成針對後者(p1)。又過了 2 秒,p1變為rejected,導緻觸發catch方法指定的回調函數。
(2)參數不是具有then方法的對象,或根本就不是對象
Promise.resolve("Success").then(function(value) {
// Promise.resolve方法的參數,會同時傳給回調函數。
console.log(value); // "Success"
}, function(value) {
// 不會被調用
});
(3)不帶有任何參數
Promise.resolve()方法允許調用時不帶參數,直接返回一個resolved狀态的 Promise 對象。如果希望得到一個 Promise 對象,比較方便的方法就是直接調用Promise.resolve()方法。
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one two
(4)參數是一個thenable對象
thenable對象指的是具有then方法的對象,Promise.resolve方法會将這個對象轉為 Promise 對象,然後就立即執行thenable對象的then方法。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
Promise.reject()方法返回一個帶有拒絕原因的Promise對象。
new Promise((resolve,reject) => {
reject(new Error("出錯了"));
});
// 等價于
Promise.reject(new Error("出錯了"));
// 使用方法
Promise.reject(new Error("BOOM!")).catch(error => {
console.error(error);
});
值得注意的是,調用resolve或reject以後,Promise 的使命就完成了,後繼操作應該放到then方法裡面,而 不應該直接寫在resolve或reject的後面 。所以,最好在它們前面加上return語句,這樣就不會有意外。
new Promise((resolve, reject) => {
return reject(1);
// 後面的語句不會執行
console.log(2);
})
let p1 = Promise.resolve(1)
let p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2)
}, 1000)
})
let p3 = Promise.resolve(3)
Promise.all([p3, p2, p1])
.then(result => {
// 返回的結果是按照Array中編寫實例的順序來
console.log(result) // [ 3, 2, 1 ]
})
.catch(reason => {
console.log("失敗:reason")
})
Promise.all 生成并返回一個新的 Promise 對象,所以它可以使用 Promise 實例的所有方法。參數傳遞promise數組中 所有的 Promise 對象都變為resolve的時候 ,該方法才會返回, 新創建的 Promise 則會使用這些 promise 的值。
如果參數中的 任何一個promise為reject的話 ,則整個Promise.all調用會 立即終止 ,并返回一個reject的新的 Promise 對象。
有時候,我們不關心異步操作的結果,隻關心這些操作有沒有結束。這時,ES2020 引入Promise.allSettled()方法就很有用。如果沒有這個方法,想要确保所有操作都結束,就很麻煩。Promise.all()方法無法做到這一點。
假如有這樣的場景:一個頁面有三個區域,分别對應三個獨立的接口數據,使用 Promise.all 來并發請求三個接口,如果其中任意一個接口出現異常,狀态是reject,這會導緻頁面中該三個區域數據全都無法出來,顯然這種狀況我們是無法接受,Promise.allSettled的出現就可以解決這個痛點:
Promise.allSettled([
Promise.reject({ code: 500, msg: '服務異常' }),
Promise.resolve({ code: 200, list: [] }),
Promise.resolve({ code: 200, list: [] })
]).then(res => {
console.log(res)
/*
0: {status: "rejected", reason: {…}}
1: {status: "fulfilled", value: {…}}
2: {status: "fulfilled", value: {…}}
*/
// 過濾掉 rejected 狀态,盡可能多的保證頁面區域數據渲染
RenderContent(
res.filter(el => {
return el.status !== 'rejected'
})
)
})
Promise.allSettled跟Promise.all類似, 其參數接受一個Promise的數組, 返回一個新的Promise, 唯一的不同在于, 它不會進行短路 , 也就是說當Promise全部處理完成後,我們可以拿到每個Promise的狀态, 而不管是否處理成功。
Promise.all()方法的效果是"誰跑的慢,以誰為準執行回調",那麼相對的就有另一個方法"誰跑的快,以誰為準執行回調",這就是Promise.race()方法,這個詞本來就是賽跑的意思。race的用法與all一樣,接收一個promise對象數組為參數。
Promise.all在接收到的所有的對象promise都變為FulFilled或者Rejected狀态之後才會繼續進行後面的處理,與之相對的是Promise.race 隻要有一個promise對象進入FulFilled或者Rejected狀态的話 ,就會繼續進行後面的處理。
// `delay`毫秒後執行resolve
function timerPromisefy(delay) {
return new Promise(resolve => {
setTimeout(() => {
resolve(delay);
}, delay);
});
}
// 任何一個promise變為resolve或reject的話程序就停止運行
Promise.race([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64)
]).then(function (value) {
console.log(value); // => 1
});
上面的代碼創建了3個promise對象,這些promise對象會分别在1ms、32ms 和 64ms後變為确定狀态,即FulFilled,并且在第一個變為确定狀态的1ms後,.then注冊的回調函數就會被調用。
ES9 新增 finally() 方法返回一個Promise。在promise結束時,無論結果是fulfilled或者是rejected,都會執行指定的回調函數。 這為在Promise是否成功完成後都需要執行的代碼提供了一種方式 。這避免了同樣的語句需要在then()和catch()中各寫一次的情況。
比如我們發送請求之前會出現一個loading,當我們請求發送完成之後,不管請求有沒有出錯,我們都希望關掉這個loading。
this.loading = true
request()
.then((res) => {
// do something
})
.catch(() => {
// log err
})
.finally(() => {
this.loading = false
})
finally方法的回調函數不接受任何參數,這表明,finally方法裡面的操作,應該是與狀态無關的,不依賴于 Promise 的執行結果。
假設有這樣一個需求:紅燈 3s 亮一次,綠燈 1s 亮一次,黃燈 2s 亮一次;如何讓三個燈不斷交替重複亮燈? 三個亮燈函數已經存在:
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
這道題複雜的地方在于 需要“交替重複”亮燈 ,而不是亮完一遍就結束的一錘子買賣,我們可以通過遞歸來實現:
// 用 promise 實現
let task = (timer, light) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (light === 'red') {
red()
}
if (light === 'green') {
green()
}
if (light === 'yellow') {
yellow()
}
resolve()
}, timer);
})
}
let step = () => {
task(3000, 'red')
.then(() => task(1000, 'green'))
.then(() => task(2000, 'yellow'))
.then(step)
}
step()
同樣也可以通過async/await 的實現:
// async/await 實現
let step = async () => {
await task(3000, 'red')
await task(1000, 'green')
await task(2000, 'yellow')
step()
}
step()
使用 async/await 可以實現用同步代碼的風格來編寫異步代碼,毫無疑問,還是 async/await 的方案更加直觀,不過深入理解Promise 是掌握async/await的基礎。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!