可以說每個前端開發者都無法避免解決異步問題,尤其是當處理了某個異步調用A后,又要緊接着處理其它邏輯,而最直觀的做法就是通過回調函數(當然事件派發也可以)處理,比如:
請求A(function (請求響應A){ //請求響應A作為參數調用方法B funB(請求響應A); });
但從業務角度來說,回調往往不會只有一層;例如我項目中有一個購物車結算的需求:我需要先給網站A下個單,然后以A請求返回的單號為參數調用另一個借口,以給網站B下一個回執單,回執單拿到之后才是跳轉頁面,大概是這樣:
下單A(function (請求響應A){ //下單A響應成功后調用下單B 下單B(function(請求響應B){ //下單B成功后跳轉 window.location.href = '我是訂單頁' }); });
如果請求再多點呢,通過回調的做法我們只能層層嵌套,這也就誕生了讓代碼維護者頭痛的回調地獄。想想幾個月后,你的同事或者自己重新閱讀這段代碼時,函數嵌套與未分離的大量業務代碼,不頭大都難!
而Promise的出現正好解決了這一痛點,通過promise我們能以鏈式調用的形式取代傳統回調嵌套的寫法,同時還能將邏輯代碼從毀掉地獄中抽離出來,改寫上面上面的例子,像這樣是不是好看了很多:
new Promise(下單A) .then(下單B(單號A)) .then(頁面跳轉)
一、基本用法
Promise強大的地方就在於能通過對異步請求狀態的改變,與我們達成一種承諾;例如將請求的狀態由pending(進行中)改為fulfilled(已完成),我們就可以通過then方法對應的回調處理相關后續邏輯了。
Promise對象的狀態一共有三種,進行中pending,已成功fulfilled(resolved)與已失敗rejected。值得一提的是,promise對象的狀態不會受外界影響,但我們可以通過內置方法對狀態進行改變。且狀態一旦改變,此狀態就會定型;直白點說就是假設我們將pending改為resolved后,不管什么時候再去訪問它,狀態將永遠維持為已完成狀態。我們通過一個例子證實這一點:
let fn = (resolve, reject) => { //改為resolved resolve(1); //再次修改狀態無效 reject(2); }; let p = new Promise(fn); //輸出1 p.then(resp => console.log(resp), err => console.log(err));
1.創建promise實例並通過then回調
Promise對象是一個構造函數,我們可以通過new來新建一個Promise實例:
function promiseDemo(resolve, reject) { if (true) { //異步請求成功 resolve(1); } else { reject(respError); }; }; let promise = new Promise(promiseDemo); console.log(promise);
在new的操作中,我們需要給Promise傳遞一個函數作為參數,且此函數中可直接使用resolve與reject參數,用於修改異步請求的狀態。
在上述代碼中,我們假設發起了一次異步請求,同時擬定true為異步請求成功,通過resolve處理請求成功的返回數據,得到了一個promise實例。通過打印可以看到狀態被修改為已成功(resolved),同時得到了響應成功數據value為1;
現在嘗試使用then處理回調,可以看到成功輸出了1,而這個1是我們擬定異步請求成功返回的數據。
promise.then(function (resp){ //異步請求成功后續邏輯 console.log(resp);//1 },function (resprror){ //請求失敗邏輯 });
那上面就是一個模擬的簡單的promise例子了,下面具體聊聊promise內置方法,看看promise具體有哪些用法。
二、Promise方法介紹
1.Promise.resolve()
Promise.resolve()能將一個對象轉為promise對象。
我在前面說,通過new Promise能得到一個promise實例對象,其實通過Promise.resolve()也能創建一個promise實例,下面這個例子可以看到得到的結果是相同的:
let promise1 = Promise.resolve(1); let promise2 = new Promise(resolve => resolve(1)); console.log(promise1, promise2);
需要注意的是Promise.resolve()接受的參數不同,處理參數的行為也將不同。
1.1傳遞一個promise對象作為參數
如果我們為Promise.resolve()傳遞一個promise對象,那么它將會將此對象原封不同的返回:
let promise1 = Promise.resolve(1); let promise2 = Promise.resolve(promise1); console.log(promise1 === promise2);//true 還是原來的味道
當我們使用promise作為參數用於創建另一個promise對象時,還需要注意promise具有狀態傳遞的特性:
const p1 = new Promise((resolve, reject) => { setTimeout(() => reject(1), 3000) }); const p2 = new Promise((resolve, reject) => { setTimeout(() => resolve(p1), 1000) }); //等待四秒后觸發reject回調,輸出失敗了 p2.then(resp => console.log('成功了'),err => console.log('失敗了'));
上述代碼中,p2的狀態由p1決定,四秒后p1 then方法還是觸發了失敗回調。
1.2傳遞一個非對象,例如數字,字符串
當我們傳遞一個非對象作為參數,Promise.resolve會返回一個promise對象,狀態為resolved,且通過then回調我們能正常訪問該參數。
let promise1 = Promise.resolve(1);//第一步執行 console.log(promise1);//第二步執行 promise1.then(resp => console.log(resp));//then方法最后執行 console.log(2);//第三步執行
在ES6入門這本書中說,由於傳遞的參數不具備異步行為(不帶有then方法),所以Promise.resove同步執行修改了參數狀態,並立刻執行then回調;但事實並非如此;如上,通過斷點將執行先后步驟加在了注釋里,打印2的操作要早於then,then依舊是最后執行,這里特別指出。
我對於這里的理解是,不管是new Promise()中的resolve()還是Promise.resolve(),只要不具備異步行為,resolve方法本身都將同步執行,但是then回調仍然是異步觸發。
一個new Promise例子:
let demo = resolve => { resolve(1); }; let promise = new Promise(demo); //此時的promise狀態已修改為resolved,說明resolve(1)已觸發 console.log(promise) promise.then(resp => { console.log(resp); }); console.log(2);
這個例子中依次打印promise,2,1。且打印出的promise對象狀態為resolved,說明resolve()方法為同步執行,但then回調最后觸發。
一個Promise.resolve()例子:
let promise = Promise.resolve(1);
console.log(promise);
promise.then(resp => console.log(resp))
console.log(2);
依舊是第一打印promise,第二次打印1,且promise狀態是resolved,說明在打印1之前,Promise.resolve(1)已經執行完成,then回調最后觸發執行。
這里我加個例子與上面的代碼做對比,讓resolve處理異步操作,下面的代碼才符合resolve完成立刻觸發then回調的情況:
let demo = resolve => { console.log(1); setTimeout(() => { resolve(4); }, 1000); console.log(2); }; let promise = new Promise(demo); promise.then(resp => console.log(resp)); console.log(3);
上述代碼依次會輸出1,2,3,4,這個例子中resolve()方法由於異步的問題等到同步代碼跑完了才觸發了,同時resolve完成立刻觸發了then方法,最后輸出了4。
1.3.傳遞一個thenable對象
thenable對象是指帶有then方法的對象,我們可以手動創建此類對象,我個人感覺angular中$http返回的對象應該也是thenable對象。
對於thenable對象resolve方法會將此對象轉為promise對象,得到的實例也能正常通過then方法回調。
let thenable = { then: (resolve, reject) => resolve(42) }; let p1 = Promise.resolve(thenable); p1.then(value => console.log(value));//42
1.4.不傳遞參數
如果不傳遞參數,則得到一個沒有value,但狀態是resolved的promise對象。
let promise = Promise.resolve();
console.log(promise);
2.Promise.reject()
Promise.reject()也會返回一個promise實例,狀態為rejected。reject接收的參數會原封不動作為reject回調時的參數,不像resolve那么多情況。
let thenable = { then: function (resolve, reject) { reject(1); } }; let p = Promise.reject(thenable); p.then(resp => { console.log(resp) }, err => { console.log(err === thenable)//true });
3.Promise.prototype.then()
為什么次方法是Promise.prototype.then()而不是Promise.then()呢,這是因為then方法是為Promise實例提供,而實例的方法是通過繼承而來,then方法在Promise對象的原型鏈上也就合情合理了。
通過前面的例子,我們也知道了then方法提供了2個回調函數,第一個對應resolved狀態,第二個對應rejected狀態。
需要注意的是,then方法會隱性返回一個新的promise實例,我們甚至可以無限使用then回調都不會報錯:
let p = Promise.resolve(1); p.then(resp => console.log(resp)) //1 .then(resp => console.log(resp)) //undefined .then(resp => console.log(resp)) //undefined // ...無數個
也正因為這個特性,我們在處理異步請求A的then回調中,可以手動返回一個異步請求B的promise實例,通過這樣的做法也就實現了同步鏈式的寫法:
let p1 = Promise.resolve(1), p2 = Promise.resolve(2), p3 = Promise.resolve(3); p1.then(resp => { return p2; }).then(resp => { return p3; }).then(resp => { console.log(resp);//3 });
4.Promise.prototype.catch()
總是推薦使用catch()方法代替then方法中的第二個回調;這是因為catch方法不僅能捕獲異步請求的錯誤,它還能捕獲then方法的錯誤,但then的第二個回調做不到這一點。
let p = Promise.resolve(1); p.then(resp => { console.log(x); }).catch(err => { console.log(err);//x is not defined });
上述代碼中我們在成功回調中故意打印一個未定義的變量x,catch成功幫我們捕獲了這個錯誤,但是如果使用then第二個錯誤回調,是無法捕獲的。
const fn = (resolve,reject) => { console.log(x); }; let p = new Promise(fn); p.then(resp => { console.log(1); }).catch(err => { console.log(err);//x is not defined });
這個例子中,我們在創建promise實例的函數中故意出錯,catch也捕獲了錯誤,雖然這個錯誤then第二個回調也能做到,但整體來說catch更為強大,這也是推薦使用catch而不是then第二個回調的理由。
說到處理錯誤,Promise還有個奇怪的地方,假設Promise出錯了,但沒使用then第二回調或者catch處理錯誤;盡管程序會報錯,但這個錯誤並不會拋出給外層,所以外層程序並不會因此停止執行,所以在then回調后面跟一個catch方法是有必要的。
const fn = (resolve, reject) => { resolve(1); }; let p = new Promise(fn); p.then(resp => { console.log(x); }); setTimeout(() => { console.log(1); }, 0);
5.Promise.prototype.finally()
finally()方法有點像switch case中的default,不管你異步成功了還是失敗了,finally都會如約而至的觸發。一般用法是這樣:
const p = Promise.resolve(1); p.then(resp => console.log(resp)) .catch(err => console.log(err)) .finally(() => console.log('執行完畢'));
finally方法因為不關心Promise狀態,所以不需要傳遞參數,在ES6入門中也提到,由於不管狀態成功或者失敗都會觸發,所以finally也等同於then方法中使用兩個回調的做法。
6.Promise.all()
Promise.all()方法接受多個promise實例,返回一個全新的promise實例:
let p = Promise.all([p1, p2, p3]);
如果p1,p2,p3不是promise實例,則會在all執行前先為這三個參數執行Promise.resolve()方法。
all方法返回的promise實例的狀態由參數共同決定,以上面代碼為例,如果三個promise實例狀態全部為resolved,則p的狀態便為resolved,但如果三個實例有一個未rejected,則p的狀態便為rejected。
//全部為resolved let p1 = Promise.resolve(1), p2 = Promise.resolve(2), p3 = Promise.resolve(3); let p = Promise.all([p1, p2, p3]); p.then(resp => console.log(resp));//[1,2,3]; //部分為reject let p1 = Promise.resolve(1), p2 = Promise.reject(2), p3 = Promise.reject(3); let p = Promise.all([p1, p2, p3]); p.then(resp => console.log(resp)) .catch((err) => console.log(err));//2;
這里有個需要注意的地方,如果一個狀態為rejected的promise實例使用了then方法的第二回調,或者使用了catch()方法,這會導致all()無法觸發自己catch()方法或者then的第二回調。
let p = Promise.reject(1).then(resp => resp, err => err); // 或者 // let p = Promise.reject(1).then(resp => resp) // .catch(err => err); Promise.all([p]) .then(resp => console.log('成功執行'))//成功執行 .catch(err => console.log('報錯啦'));
上述例子中創建promise對象時雖然使用了reject方法,但由於自身有捕獲錯誤的操作,導致實例p拿到的是then方法返回的另一個promise對象。我們可以看看狀態:
let p = Promise.reject(1).then(resp => resp, err => err);
console.log(p);
不管什么時候都應該記住,then方法也會返回一個新的promise對象。
7.Promise.race()
Promise.race()同樣是接受多個promise實例返回一個全新promise實例的方法,但與all方法不同的地方在於,決定這個promise狀態的是多個實例參數中最先改變狀態的那個。
let p = Promise.race([p1, p2, p3]);
假設p3最先改變狀態成了rejected,那么p的狀態也就是rejected。p1,p2隨后再改變狀態將不會對p實例起作用。
let p1 = Promise.reject(1); let p2 = Promise.resolve(resole => { setTimeout(() => { resole(2); }, 3000) }); Promise.race([p1, p2]) .then(resp => console.log(resp)) .catch(err => console.log(err));//1
上述例子中p1是一個同步執行狀態為rejected的promise實例,p2是異步創建狀態為resolved的實例,由於p2需要等三秒,所以最終race的實例以p1為主,這里最終輸出1。
那么到這里promise基本方法就介紹完了,一個個例子去理解以及自己鑽牛角尖也花了一點時間,接下來應該會寫下promise執行順序的文章,這個與宏任務微任務掛鈎,還有就是手寫promise需要看下,那么這篇文章就寫到這里。
如果對於JS執行機制相關有疑問,可以閱讀博主這篇博文
歡迎大家留言討論!!!