你想了解的Promise,都在這裡!!

五角六芒星_發表於2018-11-19

前言

在JavaScript語言中,程式碼都是是單執行緒執行的,正是由於這個原因,導致了JavaScript中所有的網路操作,瀏覽器事件,都必須滿足非同步執行的要求。所以非同步的各種方案開始出現並逐步合理化,簡單話!

非同步處理

在開發過程中大家使用的非同步處理方案一般包括:回撥函式(Callback)PromiseGenerator函式、async/await。這裡就主要說一下這些方案的異同:

回撥函式(Callback)

假設我們定義一個getData函式用於資料請求:

function getData(url, callback) {
  // 模擬資料請求
  setTimeout(() => {
    let res = {
      url: url,
      data: {}
    }
    callback(res)
  }, 1000)
}
複製程式碼

現在的需求是我們需要依次請求三次伺服器,並且每次請求的資料必須在上次成功的基礎上執行:

getData('/api/page/1?params=123',(res1) => {
  console.log(res1);
  getData(`/api/page/2?params=${res1.data.params}`, (res2) => {
    console.log(res2);
    getData(`/api/page/3?params=${res2.data.params}`, (res3) => {
      console.log(res3);
    })
  })
})
複製程式碼

通過上面的?,我們可以看到第一次的url:/api/page/1?params=123,第二次的url: /api/page/2?params=${res1.data.params},依賴第一次請求的資料,第三次的url:/api/page/2?params=${res2.data.params},依賴第二次請求的資料。由於我們每次的資料請求都依賴上次的請求,所以我們將會將下一次的資料請求以回撥函式的形式寫在函式內部,這其實就是我們常說的回掉地獄

Promise

同樣的需求,我們使用Promise,去實現看看:

首先我們需要先將我們的getData函式改寫成Promise的形式

  function getDataPromise(url) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        let res = {
          url: url,
          data: {}
        }
        resolve(res)
      }, 1000)
    })
  }
複製程式碼

那麼邏輯程式碼應該變成:

  getDataPromise('/api/page/1?params=123')
    .then(res1 => {
      console.log(res1);
      return getDataPromise(`/api/page/2?params=${res1.data.params}`)
    })
    .then(res2 => {
      console.log(res2);
      return getDataPromise(`/api/page/3?params=${res2.data.params}`)
    })
    .then(res3 => {
      console.log(res3);
    })
複製程式碼

這樣寫完來看,發現我們每次在資料請求成功(then)之後返回一個Promise物件,方便下次使用,這樣我們就避免了回掉地獄的出現,但是這樣其實也不算事完美,當我們的請求變得複雜的時候我們會發現我們的程式碼會變的更加複雜。

為了避免這種情況的出現 async/await應運而生。

async/await

getData函式不變,還是Promise

  function getDataPromise(url) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        let res = {
          url: url,
          data: {}
        }
        resolve(res)
      }, 1000)
    })
  }
複製程式碼

需求程式碼變成:

  async function getData () {
    let res1 = await getDataPromise('/api/page/1?params=123');
    console.log(res1);
    let res2 = await getDataPromise(`/api/page/2?params=${res1.data.params}`);
    console.log(res2);
    let res3 = await getDataPromise(`/api/page/2?params=${res2.data.params}`);
    console.log(res3);
  }

複製程式碼

怎麼樣,是不是這段程式碼閱讀起來非常舒服,其實async/await都是基於Promise的,使用async方法最後返回的還是一個Promise;實際上async/await可以看作是Generator非同步處理的語法糖,?我們就來看一下使用Generator怎麼實現這段程式碼

Generator

  // 非同步函式依舊是Promise
  function getDataPromise(url) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        let res = {
          url: url,
          data: {}
        }
        resolve(res)
      }, 1000)
    })
  }

  function * getData() {
    let res1 = yield getDataPromise('/api/page/1?params=123');
    console.log(res1);
    let res2 = yield getDataPromise(`/api/page/2?params=${res1.data.params}`);
    console.log(res2);
    let res3 = yield getDataPromise(`/api/page/2?params=${res2.data.params}`);
    console.log(res3);
  }

複製程式碼

其實可以分開來看:

  let fn = getData()
  fn.next().value.then(res1 => {
    fn.next(res1).value.then(res2 => {
      fn.next(res2).value.then( () => {
        fn.next()
      })
    })
  })

複製程式碼

上面的程式碼我們可以看到,next()每一步之行.value方法返回的都是一個Promise,所以我們可以在後面新增then方法,在then方法後面我繼續呼叫next(),知道函式執行完成。實際上上面的程式碼我們不需要手動去寫,我們可以對其封裝一下:

  function run(gen) {
    let fn = gen()

    function next(data) {
      let res = fn.next(data)
      if (res.done) return res.value
      res.value.then((info) => {
        next(info)
      })
    }
    next()
  }

  run(getData)
複製程式碼

run方法用來自動執行一步操作,其實就可以看作是Generator在進行遞迴操作;

這樣我們就將非同步操作封裝到了函式內部,其實不難發現async/awaitGenerator有很多相似的地方,只不過async/await在語義上更容易被理解。

在使用async/await的時候我們不需要在去定義run(),它內部已經給我們定義封裝好了,這也是為什麼說async/awaitGenerator非同步處理的語法糖了。

Promise

上面我們介紹了回撥函式(Callback)PromiseGenerator函式、async/await的區別,下面我們就來具體說說Promise

Promise.prototype.then()

  • 作用

thenPromise.prototype.catch() 方法都會返回 promise,它們可以被鏈式呼叫 — 一種稱為複合composition 的操作.

  • 引數

第一個引數:狀態從 pending -> fulfilled 時的回撥函式

第二個引數:狀態從 pending -> rejected 時的回撥函式

  • 返回值:新的 Promise 例項(注意不是原來的 Promise 例項

  • 特點

由於 then 方法返回一個新的 Promise 例項,所以 then 方法是可以鏈式呼叫的,鏈式呼叫的 then 方法有兩個特點:

第一:後一個 then 方法的回撥函式的引數是前一個 then 方法的返回值

第二:如果前一個 then 方法的返回值是一個 Promise 例項,那麼後一個 then 方法的回撥函式會等待該 Promise 例項的狀態改變後再執行

Promise.prototype.catch

  • 描述

catch 方法可以用於您的promise組合中的錯誤處理。

Internally calls Promise.prototype.then on the object upon which is called, passing the parameters undefined and the onRejected handler received; then returns the value of that call (which is a Promise).

大家可以看一下下面的程式碼:

const promise = new Promise(function (resolve, reject) {
    setTimeout(() => {
        reject('err')
    }, 1000)
})

promise.then(
    res => console.log('s1'),
    err => console.log('e1')
).then(
    res => console.log('s2')
).catch(
    err => console.log('e2')
)
複製程式碼
e1
s2
複製程式碼

可以發現,在第一個 then 方法執行的錯誤處理函式中捕獲到了錯誤,所以輸出了 e1,那麼這個錯誤已經被捕獲到了,也就不需要 catch 再次捕獲了,所以沒有輸出 e2,這是正常的,但問題是竟然輸出了 s2。。。。 所以為了避免這種情況程式碼應該改為:

promise.then(
    res => console.log('s1')
).then(
    res => console.log('s2')
).catch(
    err => console.log('e2')
)
複製程式碼

這樣只會輸出e2

Promise.prototype.finally

當我們想在Promise無論成功還是失敗的時候都想進行某一步操作時,可以說使用finally

promise.then(
    res => console.log('s1')
).catch(
    err => console.log('e1')
).finally(
    () => console.log('end')
)
複製程式碼

很容易能夠發現,.finally 只不過是一個成功與失敗的回撥函式相同的 .then 而已。

Promise.all(iterable);

  • 引數(iterable) 一個可迭代的物件,如 Array 或 String;

  • 返回值

    • 如果傳入的引數是一個空的可迭代物件,則返回一個已完成(already resolved)狀態的 Promise。
    • 如果傳入的引數不包含任何 promise,則返回一個非同步完成(asynchronously resolved) Promise。注意:Google Chrome 58 在這種情況下返回一個已完成(already resolved)狀態的 Promise。
    • 其它情況下返回一個處理中(pending)的Promise。這個返回的 promise 之後會在所有的 promise 都完成或有一個 promise 失敗時非同步地變為完成或失敗。 見下方關於“Promise.all 的非同步或同步”示例。返回值將會按照引數內的 promise 順序排列,而不是由呼叫 promise 的完成順序決定。

?:

  const p = Promise.all([promise1, promise2, promise3])

  p.then(
      (res) => {
          // res 是結果陣列
      }
  )
複製程式碼

只有當所有 Promise 例項的狀態都變為 fulfilled,那麼 Promise.all 生成的例項才會 fulfilled。 只要有一個 Promise 例項的狀態變成 rejected,那麼 Promise.all 生成的例項就會 rejected

Promise.race

  • 作用:與 Promise.all 類似,也是將多個 Promise 例項包裝成一個 Promise 例項。

  • 引數:與 Promise.all 相同

  • 特點:

Promise.race 方法生成的 Promise 例項的狀態取決於其所包裝的所有 Promise 例項中狀態最先改變的那個 Promise 例項的狀態。

race 函式返回一個 Promise,它將與第一個傳遞的 promise 相同的完成方式被完成。它可以是完成( resolves),也可以是失敗(rejects),這要取決於第一個完成的方式是兩個中的哪個。 如果傳的迭代是空的,則返回的 promise 將永遠等待。 如果迭代包含一個或多個非承諾值和/或已解決/拒絕的承諾,則 Promise.race 將解析為迭代中找到的第一個值。

  • 例子:請求超時
const promise = Promise.race([
    getData('/path/data'),
    new Promise((resolve, reject) => {
        setTimeout(() => { reject('timeout') }, 10000)
    })
])

promise.then(res => console.log(res))
promise.catch(msg => console.log(msg))
複製程式碼

Promise.resolve()

  • 作用:將現有物件(或者原始值)轉為 Promise 物件。

  • 引數:引數可以是任意型別,不同的引數其行為不同

    • 如果引數是一個 Promise 物件,則原封不動返回
    • 如果引數是一個 thenable 物件(即帶有 then 方法的物件),則 Promise.resolve 會將其轉為 Promise 物件並立即執行 then 方法
    • 如果引數是一個普通物件或原始值,則 Promise.resolve 會將其包裝成 Promise 物件,狀態為 fulfilled
    • 不帶引數,則直接返回一個狀態為 fulfilledPromise 物件

Promise.reject()

  • 概述

Promise.reject(reason)方法返回一個帶有拒絕原因reason引數的Promise物件。

一般通過使用Error的例項獲取錯誤原因reason對除錯和選擇性錯誤捕捉很有幫助。

  • 引數:任意引數,該引數將作為失敗的理由:
Promise.reject('err')

// 等價於
new Promise(function (resolve, reject) {
    reject('err')
})
複製程式碼

統一使用Promise

其實我們在js中可以將同步程式碼也可使用Promise


function a() {
  console.log('aaa')
}

// 等價於
const p = new Promise((resolve, rejext) => {
  resolve(a())
})
複製程式碼

That's All

或者點選Promise

相關文章