es6入門4--promise詳解


 

可以說每個前端開發者都無法避免解決異步問題,尤其是當處理了某個異步調用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執行機制相關有疑問,可以閱讀博主這篇博文

JS執行機制詳解,定時器時間間隔的真正含義

歡迎大家留言討論!!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM