徹底搞懂 javascript的Promise

一、為什麼要引入Promise?

在介紹本章之前,首先先拋出幾個問題:

  • Promise解決瞭什麼問題?
  • Promise有哪些具體的使用場景?

Promise解決瞭什麼問題?

1.回調地獄問題

在沒有Promise之前,前端獲取數據往往需要通過回調函數層層嵌套的方式來解決異步問題,例如下面這段代碼實例:

// 回調地獄實例
// 奶茶函數
function getTea(fn) {
  setTimeout(() => {
    fn('獲取到一杯奶茶')
  },2000)
}
// 面包函數
function getBread(fn) {
  setTimeout(() => {
    fn('獲取到一個面包')
  },100)
}
// 如果必須按照順序獲取,而不是根據時間,要求是先獲取到奶茶後獲取到面包。
getTea(function(data) {
  console.log(data);
  getBread(function(data) {
    console.log(data);
  }) 
})

2.可讀性問題

通過Promise我們可以將上面的代碼重寫為下面的方式,明顯這樣可讀性更高。

// 下面解釋下,如何通過Promise來解決回調地獄的問題
function getTea() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('獲取到一杯奶茶')
    }, 2000)
  })
}
function getBread() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('獲取到一個面包')
    }, 500)
  })
}
getTea()
  .then(res => {
    console.log(res);
    return getBread();
  })
  .then(res => {
    console.log(res);
  })

3.信任問題(也叫回調多次執行問題)

傳統的回調函數無法保證隻被執行一次,回調函數還要可能被執行其他操作,而Promise調用且僅調用一次resolve,不會產生回調多次執行的問題,所以Promise很好的解決瞭第三方庫多次調用回調的問題。

Promise有哪些具體的使用場景?

  • 場景1:將圖片的加載寫成一個Promise,圖片一旦加載完成,Promise的狀態就會發生變化。
  • 場景2:當下一個異步請求需要依賴上一個請求結果的時候,可以通過鏈式操作解決問題。
  • 場景3:通過all()實現多個請求合並在一起,匯總所有的請求結果,隻需設置一個loading即可。
  • 場景4:通過race()可以設置圖片請求超時。

二、手寫Prromise身上的方法

手寫Promise.all

Promise.all的特點是接收的是一個可迭代對象,當這個可迭代對象中的所有元素都執行成功會返回一個數組,一個出錯則立即返回錯誤。

function myPromiseAll(iterable) {
  // 首先明確要返回的對象是一個Promise
  return new Promise((resolve,reject) => {
    // 首先將可迭代對象轉換為數組
    const promises = Array.from(iterable);
    let flag = 0;
    const result = [];
    // 開始遍歷執行
    for (let i = 0; i < promises.length; i++) {
      Promise.resolve(promises[i]).then(res => {
        result[i] = res;
        flag++;
        if (flag === promises.length) {
          resolve(result)
        }
      }).catch(err => {
        reject(err)
      })
    }
  })  
}

手寫Promise.race

Promise.race函數接收的是一個可迭代對象,相當於讓這個可迭代對象中的所有promise對象進行賽跑,隻要有一個promise對象發生瞭狀態變化,那麼直接返回這個promise對象返回的結果。

// 手寫promise.race
function myPromiseRace(iterator) {
  // 首先返回的是一個promise對象
  return new Promise((resolve,reject) => {
    for (let item of iterator) {
      Promise.resolve(item).then(res => {
        resolve(item);
      }).catch(err => {
        reject(err);
      })
    }
  })
}
let p1 = new Promise(resolve => {
  setTimeout(resolve, 105, 'p1 done')
})
let p2 = new Promise(resolve => {
  setTimeout(resolve, 100, 'p2 done')
})
myPromiseRace([p1, p2]).then(data => {
  console.log(data); // p2 done
})

手寫Promise.finally

Promise.finally的特點

  • 無論成功還是失敗,都會執行這個方法
  • 返回的是一個Promise

Promise.finally執行的例子

let p = new Promise((resolve,reject) => {
  setTimeout(() => {
    resolve(111);
  },2000)
})
p.then(res => {
  console.log(res);  // 111
}).finally(() => {
  console.log('無論如何這裡都會被執行');  // 無論如何這裡都會被執行
})

手寫Promise.finally(Promise.finally返回的本質上是一個then方法,需要在then方法中執行我們傳入的參數,然後返回形參)

Promise.prototype.finally = function(f) {
  return this.then((value) => {
    return Promise.resolve(f()).then(() => value)
  },(err) => {
    return Promise.resolve(f()).then(() => {
      throw err;
    })
  })
}

Promise.all和Promise.race的區別

Promise.all()成功和失敗的返回值是不同的,成功的時候返回的是一個結果數組,而失敗的時候返回的是最先被reject的值。當Promise.all()的結果是成功的時候,返回結果的數組裡邊的數據順序和Promise.all()接收到的promise順序是一致的。

promise.race表示多個Promise賽跑的意思,裡面哪個結果執行的快就返回哪個結果,不管結果本身是成功還是失敗,其他Promise代碼還會執行,隻是不會返回。

Promise.all和Promise.race的應用場景

promise.all()的應用場景

多個異步任務都得到結果時,進行顯示的場景

比如,當用戶點擊按鈕時,會彈出一個對話框,這個對話框中的數據來自兩個不同的後端接口獲取的數據,當用戶剛點擊的時候,顯示的時數據加載中的狀態,當這兩部分數據都從接口獲取到數據的時候,才讓數據加載中的狀態消失,此時就可以使用Promise.all方法。

Promise.race()的應用場景

提示用戶請求超時

比如,當用戶點擊按鈕發送請求的時候,當後端的接口超過我們設定的時間還沒有獲取到數據的時候,我們就可以提示用戶請求超時。

三、Promise是如何解決串行和並行的?

什麼是並行?什麼是串行?

並行:指的是多個異步請求同時進行。

串行:一個異步請求完成之後再進行下一個請求。

Promise實現並行請求

Promise實現並行請求主要是依靠Promise.all方法和Promise.race方法,我們可以通過手寫Promise.all方法或Promise.race方法來實現這一目標。

Promise實現串行請求

Promise實現串行請求主要是借助reduce函數。可以參考我的這篇文章如何控制Promise的串行執行?

// 借助reduce函數來實現Promise的串行執行
const funcArr = [
  () => {
    return new Promise((resolve) => {
      setTimeout(() => {resolve(1)},2000)
    })
  },
  () => {
    return new Promise((resolve) => {
      setTimeout(() => {resolve(2)},1000)
    })
  },
  () => {
    return new Promise((resolve) => {
      setTimeout(() => {resolve(3)},3000)
    })
  },
];
function inOrder(arr) {
  const res = [];
  return new Promise((resolve) => {
    arr.reduce((pre,cur) => {
      return pre.then(cur).then(data => res.push(data))
    },Promise.resolve()).then(data => resolve(res))
  })
}
inOrder(funcArr).then(data => console.log(data))   // [1,2,3]

四、什麼是Promise穿透?

所謂的Promise的值穿透指的是.then或者.catch的參數希望是函數,如果傳入的不是函數,則可能會發生值穿透。Promise方法通過return傳值,沒有return就隻是相互獨立的任務而已。看看下面這個例子的輸出可能會更好的幫助我們理解什麼是值穿透?

Promise.resolve(1)
  .then(function(){return 2})
  .then(Promise.resolve(3))
  .then(console.log)   // 2

之所以發生瞭值穿透就是因為第二個then中傳入的不是一個函數的形式。

五、使用Promise封裝Ajax請求

使用Promise封裝Ajax請求的關鍵步驟,全部在下面的代碼中的註釋裡,詳情請看下面的代碼。

// 使用Promise封裝Ajax請求
const res = new Promise((resolve,reject) => {
  // 1. 創建一個XMLHttpRequest對象
  const xhr = new XMLHttpRequest();
  // 2. 初始化請求方法和URL
  xhr.open('GET','https://api.apiopen.top/getJoke');
  // 3. 發送請求
  xhr.send();
  // 4. 綁定事件,處理響應結果
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
      // 這裡4代表的就是說服務端返回瞭全部的結果
      // 如果服務端返回的狀態碼是2開頭的,我們就resolve這個返回的結果,反之則reject對應的狀態碼
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.response)
      } else {
        reject(xhr.status)
      }
    }
  }
})
res.then(function(value) {
  console.log(value);
},function(err) {
  console.log(err);
})

六、Promise有哪些狀態?

Promise主要有以下三種狀態:

  • pending狀態(初始狀態)
  • fulfilled狀態(已經成功的狀態)
  • rejected狀態(已經失敗的狀態)

Promise狀態的變化過程

1.從pending到fulfilled狀態的切換

resolve前是pending狀態,resolve之後是fulfilled狀態

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('resolve前的狀態:', p);
    resolve();
    console.log('resolve之後的狀態', p);
  })
})

image.png

2.從pending狀態到rejected狀態

reject前是pending狀態,reject之後是rejected狀態。

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('reject前的狀態:', p);
    reject();
    console.log('reject之後的狀態', p);
  })
})

image.png

七、將callback改寫成Promise

1.傳統callback的形式

const fs = require('fs');
fs.readFile('./temp.md',(err,data) => {
  console.log(data.toString());
})

2.將callback改為promise的形式

核心就是通過resolve來獲取callback的數據。

const fs = require('fs');
async function myReadFile() {
  let result = await new Promise((resolve,reject) => {
    fs.readFile('./temp.md',(err,data) => {
      resolve(data.toString());
    })
  })
  console.log(result);   // xxxxx
  return result;
}
myReadFile()

總結

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!   

推薦閱讀: