1. 程式人生 > 實用技巧 >promise 詳解

promise 詳解

在面試中promise基本都會被問到,有簡單理解的,也有問到一些比較複雜的實現,有些我是知道的但也有些是不清楚的,所以查閱資料,把關於 promise 的知識補充提升一下。

本文旨在說明 promise,並未實現具體的 ajax 請求。

promise 的出現解決了什麼樣的問題

回撥地獄

假設一種業務場景,你需要取到所有開源庫的列表(假設列表本身按照開源庫熱度排序),你需要讀取當中熱度第一的庫名稱,然後通過呼叫查詢介面查出該庫官方文件地址

ajax('aaa', success(res) {
  const lib = res
  // ...other code
  ajax(`bbb`, success(res) {
    
// ... other code }) })

事實上,真實的業務可能比例子複雜很多,而後一步的操作又必須基於上一步操作的結果,我們不得不將程式碼組織成這樣的回撥。然而這樣的程式碼存在很大的問題:

  • 程式碼無法執行 return
  • 回撥層數過多會導致邏輯很難讀懂,並且後期維護難度很大

條件返回

在視訊網站或者直播網站很常見到一種場景,視訊會分多條線路(主線路、備用線路1、備用線路2…),業務上,開啟網站的時候,會同時去請求這三個視訊的介面,只要其中一個介面返回了資料,中斷其他介面的請求。這樣的業務需求在之前的方法中都沒有很好地實現方法。

promise 詳解

基本語法 && 成功處理

new Promise((resolve, reject)=> {
  resolve('success')
  reject('error')
})
.then(
  res=> { console.log('success', res) }
  err=> { console.log('error', err) }
)
.catch(err=> { console.log(err) })

  • promise 有三個狀態
    • pending 等待結果返回(未完成)
    • fulfilled 實現(操作完成)
    • rejected 被拒絕(操作失敗)
    • 狀態一旦改變,不會再次改變
  • 因此 promise 狀態狀態改變只有兩種可能
    • pending => fulfilled
    • pending => rejected

其中 pending => fulfilled 表示操作由未完成變為成功,這時會觸發resolve,將操作的結果作為引數傳遞出去,pending => rejected 表示操作由未完成變為成功,這時會觸發reject,將操作結果的錯誤資訊作為引數傳遞出去。
必須注意的是,promise 會將狀態傳遞出去,用於下一步驟操作的引數

new Promise(resolve=> {
  ajax('aaa', success(data) {
    // data = {name: 'singleDogNo.1'}
    resolve(data)
  })
}).then(res=> {
  return new Promise(resolve=> {
    ajax('bbb', success(data) {
      // data = {age: 27}
      resolve(...[res, data])
    })
  })
}).then(res=> {
  console.log(res)
  /*
    {
      name: 'singleDogNo.1',
      age: 27
    }
  */
})

錯誤處理

promise 會自動捕捉異常,交給 rejected 函式處理

new Promise(resolve=> {
  setTimeout(()=> {
    throw new Error('error!')
  })
})
.then(res=> {
  console.log(res)
})
.catch(err=> {
  console.log(err) // error!
})

這裡需要注意的是鏈式執行非同步操作時,你可以選擇為每一步操作做錯誤處理,類似a().then(b()).catch().then(c()).catch(),也可以將錯誤處理放在最後執行,類似a().then(b()).then(c()).catch()。但一般推薦第二種方式,更加方便閱讀。

還有一點需要注意的是 catch 本身也會返回 promise 例項,並且狀態是 resolve,而且一旦執行到 catch 中,鏈式操作將會中斷,不再繼續執行。

new Promise(resolve=> {
  setTimeout(()=> {
    resolve()
  }, 1000)
})
.then(()=> {
  console.log('promise1')
  throw new Error('error')
})
.then(()=> {
  console.log('promise2')
})
.then(()=> {
  console.log('promise3')
})
.catch(err=> {
  console.log(err)
})
// promise1
// Error: error
// 不會繼續執行 promise2 和 promise3

promise.all()

這個方法會在所有非同步操作執行完成並且狀態全部為成功的時候執行回撥方法

function randomA() {
  return new Promise((resolve,reject)=> {
    setTimeout(()=> {
      const num = Math.floor(Math.random() * 10)
      console.log('num: ', num);
      if (num <= 5) {
        resolve(num)
      } else {
        reject('randomA:數字大於5是不行的')
      }
    }, 1000)
  })
}

function randomB() {
  return new Promise((resolve,reject)=> {
    setTimeout(()=> {
      const num = Math.floor(Math.random() * 10)
      console.log('num: ', num);
      if (num <= 5) {
        resolve(num)
      } else {
        reject('randomB:數字大於5是不行的')
      }
    }, 1000)
  })
}

function randomC() {
  return new Promise((resolve,reject)=> {
    setTimeout(()=> {
      const num = Math.floor(Math.random() * 10)
      console.log('num: ', num);
      if (num <= 5) {
        resolve(num)
      } else {
        reject('randomC:數字大於5是不行的')
      }
    }, 1000)
  })
}

Promise.all([randomA(), randomB(), randomC()]).then(res=> {
  console.log(res)
  // success: [1,2,3]
  // error: Uncaught (in promise) randomB:數字大於5是不行的
})

可以複製上面程式碼執行,只有當三個方法值全部小於 5,才會返回正確的值。可以看到正確返回時,返回值是陣列的形式。陣列中每一項對應 all 方法中的每一個非同步操作的結果。

promise.race()

回想一下上面描述的視訊平臺切換可選線路的問題。在 promise 中可以使用promise.race()方法解決。這個方法完全區別於promise.all(),上面的方法在所有非同步操作完成之後才執行,這個方法則是誰先完成就先處理誰的回撥方法。先執行完的方法無論成功或失敗,其餘的操作還會繼續執行,但是不會進入 race 的回撥方法。

// promise.all 的例子,將 timeout 區分開來
function randomA() {
  return new Promise((resolve,reject)=> {
    setTimeout(()=> {
      const num = Math.floor(Math.random() * 10)
      console.log('num: ', num);
      if (num <= 5) {
        resolve(num)
      } else {
        reject('randomA:數字大於5是不行的')
      }
    }, 1000)
  })
}

function randomB() {
  return new Promise((resolve,reject)=> {
    setTimeout(()=> {
      const num = Math.floor(Math.random() * 10)
      console.log('num: ', num);
      if (num <= 5) {
        resolve(num)
      } else {
        reject('randomB:數字大於5是不行的')
      }
    }, 2000)
  })
}

function randomC() {
  return new Promise((resolve,reject)=> {
    setTimeout(()=> {
      const num = Math.floor(Math.random() * 10)
      console.log('num: ', num);
      if (num <= 5) {
        resolve(num)
      } else {
        reject('randomC:數字大於5是不行的')
      }
    }, 3000)
  })
}

Promise.race([randomA(), randomB(), randomC()]).then(
  res=> {
    console.log('res: ', res)
  },
  err=> {
    console.log('err: ', err)
  }
)

利用promise.race實現需求 - 如果介面 10s 內返回資料就處理資料,否則執行其他操作

function getData() {
  return new Promise(resolve=> {
    ajax('url', success(res) {
      resolve()
    })
  })
}

function timeout() {
  return new Promise((resolve, reject)=> {
    setTimeout(()=> {
      reject('請求超時')
    }, 10000)
  })
}

Promise.race([getData(), timeout()])
.then(res=> {
  console.log(res)
})
.catch(err=> {
  console.log(err)
})