前端- JavaScript非同步程式設計Promise

程式設計碼農發表於2021-10-27

什麼是Promise

Promise 是非同步程式設計的一種解決方案。ES6中已經提供了原生Promise物件。一個Promise物件會處於以下幾種狀態(fulfilled,rejected兩種狀態一旦確定後不會改變):

  • 待定(pending): 初始狀態,既沒有被兌現,也沒有被拒絕。
  • 已兌現(fulfilled): 意味著操作成功完成。
  • 已拒絕(rejected): 意味著操作失敗。

基本用法

Promise物件是一個建構函式,用來建立Promise例項,它接收兩個引數resolvereject

  • resolve的作用是將Promise物件的狀態從pending變為fulfilled,在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去。
  • reject的作用是將Promise物件的狀態從pending變為rejected,在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
const promise = new Promise(function(resolve, reject) {
  // ... 
  if (/* 非同步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise例項生成以後,使用then方法分別指定fulfilled狀態和rejected狀態的回撥函式。

  • then接收兩個引數,第一個是Promise物件的狀態變為fulfilled時的回撥函式,第二個是狀態變為rejected時的回撥函式。
  • catch接收Promise物件的狀態變為rejected時的回撥函式。
promise.then(function (value){
    // ....
},function (err){
    // .... err
})
  
promise.then(function (value){
    // ....
}).catch(function (err){
    // ....
})

Promise的方法

Promise.prototype.then()

then方法是定義在原型物件Promise.prototype上,前面說過,它接收兩個可選引數,第一個引數是fulfilled狀態的回撥函式,第二個引數是rejected狀態的回撥函式。

then方法返回的是一個新的Promise例項,方便我們採用鏈式寫法。比如then後面接著寫then,當第一個回撥函式完成以後,會將返回結果作為引數,傳入第二個回撥函式。這種鏈式方式可以很方便的指定一組按照次序呼叫的回撥函式。

loadData().then(function (value){
    return 3
}).then(function (num){
    console.log("ok", num) // 3
})

Promise.prototype.catch()

catch方法是用於指定發生錯誤時的回撥函式。如果非同步操作丟擲錯誤,狀態就會變為rejected,就會呼叫catch()方法指定的回撥函式,處理這個錯誤。

const promise = new Promise(function(resolve, reject) {
  throw new Error('unkonw error'); // 丟擲錯誤狀態變為 -> reject
});
const promise = new Promise(function(resolve, reject) {
  reject('unkonw error') // 使用reject()方法將狀態變為 -> reject
});
promise.catch(function(error) {
  console.log(error);
});

Promise物件的錯誤會一直向後傳遞,直到被捕獲為止。比如下面程式碼:catch會捕獲loadData和兩個then裡面丟擲的錯誤。

loadData().then(function(value) {
  return loadData(value);
}).then(function(users) {
 
}).catch(function(err) {
  // 處理前面三個Promise產生的錯誤
});

如果我們不設定catch(),當遇到錯誤時Promise不會將錯誤丟擲外面,也就是不會影響外部程式碼執行。

const promise = new Promise(function(resolve, reject) {
      resolve(a) // ReferenceError: a is not defined
});
promise.then(function(value) {
  console.log('value is ', value)
});
setTimeout(() => { console.log('code is run') }, 1000); // code is run

Promise.prototype.finally()

finally()方法不管 Promise 物件最後狀態如何,都會執行的操作。下面是我們使用 Promise 的一個常規結構。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

Promise.all()

Promise.all()方法可以將多個 Promise 例項包裝成一個新的 Promise 例項返回。

const promise = Promise.all([p1, p2, p3]);

promise狀態來依賴於“傳入的promise”。

  • 只有當所有“傳入的promise”狀態都變成fulfilled,它的狀態才會變成fulfilled,此時“傳入的promise”返回值組成一個陣列,傳遞給promise的回撥函式。
  • 如果“傳入的promise”之中有一個被rejected,新promise的狀態就會變成rejected,此時第一個被rejectpromise的返回值,會傳遞給promise的回撥函式。
const promises = [1,2,3,4].map(function (id) {
  return loadData(id);
});

Promise.all(promises).then(function (users) {
  // ...
}).catch(function(err){
  // ...
});

Promise.race()

Promise.race()方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。

Promise.race()方法的引數與Promise.all()方法一樣。

const promise = Promise.race([p1, p2, p3]);

Promise.all()Promise.race()對比:

  • Promise.all() ,如果所有都執行成功則返回所有成功的promise值,如果有失敗則返回第一個失敗的值。
  • Promise.race(),返回第一個執行完成的promise 值,它可能是fulfilled和rejected狀態。

這兩個方法的使用場景。

場景一,使用者登入社交網站主頁後,會同時非同步請求拉取使用者資訊,關注列表,粉絲列表,我們需要保證所有資料請求成功再進行渲染頁面,只要有一個資料不成功就會重定向頁面,這裡可以使用Promise.all

function initUserHome() {
  Promise.all([
  new Promise(getMe),
  new Promise(getFollows),
  new Promise(getFans)
])  
    .then(function(data){
        // 顯示頁面
  })
    .catch(function(err){
    // .... 重定向頁面
  });
};

initUserHome();

場景二,假如我們在做一個搶票軟體,雖然請求了很多賣票渠道,每次只需返回第一個執行完成的Promise,這裡可以使用Promise.race

function getTicket() {
  Promise.race([
  new Promise(postASell),
  new Promise(postBSell),
  new Promise(postCSell)
])  
    .then(function(data){
        // 搶票成功
  })
    .catch(function(err){
    // .... 搶票失敗,重試
  });
};

getTicket();

Promise.allSettled()

使用Promise.all()時,如果有一個Promise 失敗後,其它Promise 不會停止執行。

const requests = [
  fetch('/url1'),
  fetch('/url2'),
  fetch('/url3'),
];

try {
  await Promise.all(requests);
  console.log('所有請求都成功。');
} catch {
  console.log('有一個請求失敗,其他請求可能還沒結束。');
}

有的時候,我們希望等到一組非同步操作都結束了,再進行下一步操作。這時就需要使用Promise.allSettled(),的它引數是一個陣列,陣列的每個成員都是一個 Promise 物件,返回一個新的 Promise 物件。它只有等到引數陣列的所有 Promise 物件都發生狀態變更(不管是fulfilled還是rejected),返回的 Promise 物件才會發生狀態變更。

const requests = [
  fetch('/url1'),
  fetch('/url2'),
  fetch('/url3'),
];

await Promise.allSettled(requests);
console.log('所有請求完成後(包括成功失敗)執行');

Promise.any()

只要傳入的Promise有一個變成fulfilled狀態,新的Promise就會變成fulfilled狀態;如果所有傳入的Promise都變成rejected狀態,新的Promise就會變成rejected狀態。

Promise.any()Promise.race()差不多,區別在於Promise.any()不會因為某個 Promise 變成rejected狀態而結束,必須等到所有引數 Promise 變成rejected 狀態才會結束。

回到Promise.race()多渠道搶票的場景,如果我們需要保證要麼有一個渠道搶票成功,要麼全部渠道都失敗,使用Promise.any()就顯得更合適。

function getTicket() {
  Promise.any([
  new Promise(postASell),
  new Promise(postBSell),
  new Promise(postCSell)
])  
    .then(function(data){
        // 有一個搶票成功
  })
    .catch(function(err){
    // .... 所有渠道都失敗了
  });
};

getTicket();

Promise.resolve()

Promise.resolve()方法將現有物件轉換為Promise物件。等價於下面程式碼:

new Promise(resolve => resolve(1))

傳入的引數不同,處理

  1. 引數Promise 例項,它將不做任何修改、原封不動地返回這個例項。
  2. 引數thenable物件,它會將這個物件轉為 Promise 物件,然後就立即執行thenable物件的then()方法。

    let thenable = {
      then: function(resolve, reject) {
        resolve(1);
      }
    };
  3. 引數是普通值,返回一個新的 Promise 物件,狀態為resolved

    const promise = Promise.resolve(1);
    
    promise.then(function (value) {
      console.log(value) // 1
    });
  4. 無引數,直接返回一個resolved狀態的 Promise 物件。

Promise.reject()

Promise.reject(reason)方法也會返回一個新的 Promise 例項,該例項的狀態為rejected

const promise = Promise.reject('unkonw error');
// 相當於
const promise = new Promise((resolve, reject) => reject('unkonw error'))

promise.then(null, function (s) {
  console.log(s)
});
// unkonw error

簡單場景

非同步載入圖片:

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
      const image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = url;
  });
}

請求超時處理:

//請求
function request(){
    return new Promise(function(resolve, reject){
          // code ....
         resolve('request ok')
    })
}

function timeoutHandle(time){
         return new Promise(function(resolve, reject){
        setTimeout(function(){
            reject('timeout');
        }, time);
    });
}

Promise.race([
    request(),
    timeoutHandle(5000)
])
.then(res=>{
    console.log(res)
}).catch(err=>{
    console.log(err)// timeout
})

小結

本文歸納了Promise相關方法和一些簡單用法,歡迎留言交流。

相關文章