淺談 Promise

baihu發表於2019-02-16

JavaScript 解決非同步程式設計有兩種主要方式:事件模型和回撥函式。但是隨著業務越來越複雜,這兩種方式已經不能滿足開發者的需求了,Promise 可以解決這方面的問題。

為了更好的理解 Promise 是如何工作的,我們先來了解一下傳統的非同步程式設計的方式。

非同步程式設計的方式

1. 事件模型:

let button = document.getElementId("my-btn");
button.onclick = function() {
    console.log("Hello");
}

任務的執行不取決於程式碼的順序,而取決於某個事件是否發生。上面程式碼中,console.log(“Hello”) 直到 button 被點選後才會被執行。當 button 被點選,賦值給 onclick 的函式就被新增到作業佇列的尾部,並在佇列前部所有任務結束之後再執行。

事件模型適用於處理簡單的互動,若將多個獨立的非同步呼叫連線在一起,必須跟蹤每個事件的事件目標,會使程式更加複雜,執行流程會變得很不清晰。

2. 回撥函式

我們來看一下比較經典的使用 jsonp 解決跨域問題的示例:

function callback (res) {
  document.getElementById(`d1`).innerHTML = res.result.address
  console.log(`Your public IP address is: `, res)
}
function jsonp (lat, lng) {
  let src = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=yourKey&output=jsonp&callback=callback`
  let script = document.createElement(`script`)
  script.setAttribute("type","text/javascript")
  script.src = src;
  document.body.appendChild(script)
}
jsonp(39.92, 116.43)

初看這種模式運作得相當好,簡單、容易理解,但你可能會迅速的發現這樣的模式不利於程式碼的閱讀和維護,各個部分之間耦合度太高,容易陷入回撥地獄。就像這樣:

method1(function(err, result) {
  if (err) {
    throw err
  }

  method2(function(err, result) {
    if (err) {
      throw err
    }

    method3(function(err, result) {
      if (err) {
        throw err
      }

      method4(function(err, result) {
        if (err) {
          throw err
        }

        method5(result)
      })
    })
  })
})

Promise 能大幅度改善這種情況。我們來看下Promise 是如何實現的:

let promise = new Promise((resolve, reject) => {
  // ... method 1 some code

  if (/* 非同步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
})
promise.then((value) => {
  // method 2 some code
}).then((value) => {
  // method 3 some code
}).then((value) => {
  // method 4 some code
}).then((value) => {
  // method 5 some code
}).catch((err) => {
  // some err code
})

是不是清晰很多?是不是很神奇?接下來一起來學習一下 Promise 吧!

Promise 相關知識

1. Promise 的基礎知識

我們先來看下 Promise 的方法有哪些:

Promise.Prototype.then()
Promise.Prototype.catch()
Promise.Prototype.finally()
Promise.all()
Promise.race()
Promise.resolve()
Promise.reject()
Promise.try()

Promise 函式的執行,都是依賴於狀態的改變,這三種狀態要記牢哦:

Pending:進行中
Fulfilled:已成功
Rejected:已失敗

Promise 優點:

1)物件的狀態不受外界影響。
2)一旦狀態改變,就不會再變,任何時候都可以得到這個結果。
3)將非同步操作以同步操作的流程表達出來,避免了層層巢狀回撥函式。
4)提供統一的介面,使得控制非同步操作更加容易。

Promise 缺點:

1)無法取消 Promise,一旦新建它就會立即執行,無法中途取消。
2)如果不設定回撥函式,Promise 內部丟擲的錯誤,不會反映到外部。
3)當處於 pending 狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。

瞭解了 Promise 的方法,3種狀態以及特點和優缺點之後,接下來我們來看一下 Promise 是怎麼使用的。

2. Promise 的基本用法

我們來創造一個讀取檔案的 Promise 例項:

const fs = require(`fs`)
const path = require(`path`)

function readFile (filename) {
  return new Promise (function (resolve, reject) {
    // reject(new Error(`err`))
    //觸發非同步操作
    fs.readFile(filename, {encoding: `utf8`}, function (err, contents) {

      // 檢查錯誤
      if (err) {
        reject(err)
        return
      }

      //讀取成功
      resolve(contents)
    })
  })
}
let promise = readFile(path.resolve(__dirname, `../json/a.json`))

上述例項中 resolve 函式的作用是,將 Promise 物件的狀態從“未完成”變為“成功”(即從 pending 變為 resolved),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;

reject 函式的作用是,將 Promise 物件的狀態從“未完成”變為“失敗”(即從 pending 變為 rejected),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。

2.1 Promise.Prototype.then()

Promise 例項生成以後,就可以用 then 方法來分別指定 resolved 狀態和 rejected 狀態的回撥函式了。

promise.then(function (contents) {
  // 完成
  console.log(contents)
  return(contents)
}, function (err) {
  // 失敗
  console.log(err.message)
})

我們可以看到,then 方法接受兩個回撥函式作為引數;
第一個回撥函式在 Promise 物件的狀態變為 resolved 時呼叫;
第二個回撥函式在 Promise 物件的狀態變為 rejected 時呼叫;
其中,第二個函式是可選的。這兩個函式都接受 Promise 物件傳出的值作為引數。

Promise.then 方法每次呼叫,都返回一個新的 Promise 物件,所以支援鏈式寫法。

let taskA = (value) => {
  console.log("Task A")
  console.log(value)
  return value
}

let taskB = (value) => {
  console.log("Task B")
  console.log(value)
}

promise
.then(taskA)
.then(taskB)
.catch((err) => {
  console.log(err.message)
})

2.2 Promise.Prototype.catch()

Promise.prototype.catch 方法是 .then(null, rejection) 的別名,相當於 then 函式的第一個引數傳入 null,第二個引數傳入發生錯誤時的回撥函式。

promise.then(function(value) {
  // 成功
  console.log(value)
}).catch(function (err) {
  // 失敗
  console.log(err.message)
})

2.3 Promise.Prototype.finally()

finally 方法用於指定不管 Promise 物件最後狀態如何,都會執行的操作。該方法是 ES2018 引入標準的,目前大部分瀏覽器還不支援,不過可以自己實現。

finally 方法的實現:

Promise.prototype.finally = function (callback) {
  let P = this.constructor
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  )
}

finally 方法的使用:

promise
.then((contents) => {
  console.log(contents)
  return contents
})
.catch((err) => {
  console.log(err.message)
})
.finally(() => {
    console.log(`finally`)
})

2.4 Promise.all()

Promise.all 方法可以將多個 Promise 例項,包裝成一個新的 Promise 例項。

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

新的 Promise p 的狀態由 p1、p2、p3 決定,只有當 p1、p2、p3 的狀態都變成了 fulfilled,p 的狀態才會變成 fulfilled;只要 p1、p2、p3 之中有一個被 rejected,p 的狀態就變成了 rejected。

注意,如果作為引數的 Promise 例項,自己定義了 catch 方法,那麼它一旦被 rejected,並不會觸發Promise.all()的 catch 方法的。

如果 p2 有自己的 catch 方法,就不會呼叫 Promise.all() 的 catch 方法。

const p1 = new Promise((resolve, reject) => {
  resolve(`hello`);
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error(`報錯了`);
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 報錯了]

如果 p2 沒有自己的 catch 方法,就會呼叫 Promise.all() 的 catch 方法。

const p1 = new Promise((resolve, reject) => {
  resolve(`hello`);
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
  throw new Error(`報錯了`);
})
.then(result => result);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 報錯了

2.5 Promise.race()

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

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

新的Promise p,只要 p1、p2、p3 之中有一個例項率先改變狀態,p 的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給 p 的回撥函式。

function timerPromisefy(delay) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(delay)
    }, delay)
  })
}

Promise.race([
  timerPromisefy(10),
  timerPromisefy(20),
  timerPromisefy(30)
]).then(function (values) {
  console.log(values) // 10
})

2.6 Promise.resolve()

有時需要將現有物件轉為 Promise 物件,Promise.resolve 方法就起到這個作用,返回一個 fulfilled 狀態的 Promise 物件。

const promise = Promise.resolve(`hello`);
promise.then(function(value){
    console.log(value);
});

// 相當於
const promise = new Promise(resolve => {
   resolve(`hello`);
});
promise.then((value) => {
  console.log(value)
})

2.7 Promise.reject()

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

const p = Promise.reject(`出錯了`);
p.then(null, (value) => {
  console.log(value)
})

// 等同於
const p = new Promise((resolve, reject) => reject(`出錯了`))
p.then(null, (value) => {
  console.log(value)
})

2.8 Promise.try()

讓同步函式同步執行,非同步函式非同步執行。

const f = () => console.log(`now`);
Promise.try(f);
console.log(`next`);
// now
// next

應用

非同步載入圖片

實現方法:

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image()

    image.onload = function() {
      resolve(image)
    }

    image.onerror = function() {
      reject(new Error(`Could not load image at ` + url))
    }

    image.src = url
  })
}

呼叫:

loadImageAsync(`圖片路徑`).then((value) => {
  document.getElementById(`d1`).appendChild(value)
}).catch((err) => {
  console.log(err.message)
})

非同步載入 js

實現方法:

let loadScript = function () {
  return function _loadScript(url, callBack) {
    return new Promise(function (resolve) {
      let script = document.createElement(`script`)
      script.type = `text/javascript`
      if (script.readyState) {
        // 相容IE的script載入事件
        script.onreadystatechange = function () {
          // loaded : 下載完畢 complete: 資料準備完畢。這兩個狀態ie可能同時出現或者只出現一個
          if (script.readyState === `loaded` || script.readyState === `complete`) {
            // 防止載入兩次
            script.onreadystatechange = null
            callBack()
            // 把函式傳遞下去,保證能順序載入js
            resolve(_loadScript)
          }
        }
      } else {
        script.onload = function () {
          callBack()
          resolve(_loadScript)
        }
      }
      script.src = url
      document.head.appendChild(script)
    })
  }
}()

呼叫:

loadScript(`http://code.jquery.com/jquery-3.2.1.min.js `, () => {})
    .then(() => {
      $("#d1").on(`click`, () => {alert(1)})
    }).catch((err) => {
      console.log(err)
    })

request 請求的封裝

import axios from `./axios`
import qs from `qs`

const config = {
  time: +new Date() + ``,
  timeout: 6000,
  headers: {
    `Content-Type`: `application/x-www-form-urlencoded`,
    time: new Date().getTime()
  }
}

function checkResponse (response, notice) {
  return new Promise((resolve, reject) => {
    let code = Number(response.code)
    if (code === 0 || code === 200 || code === 2000 || code === 1 || code === 2 || code === `0` || code === 109) {
      resolve(response)
    } else {
      if (notice) {
        // 提示資訊
        console.log(`response-notice`, notice)
      }
      reject(response)
    }
  })
}

function fixURL (url, type) {
  let result = ``
  switch (type) {
    case `r`:
      result += `/api/v2${url}`
      break
  }
  return result
}

/**
 * Requests a URL, returning a promise.
 *
 * @param  {object} [options]         The options we want to pass to axios
 * @param  {string} [options.url]     請求的url地址(必須)
 * @param  {string} [options.method]  請求方式, get or post,預設post
 * @param  {object} [options.data]    請求引數
 * @param  {number} [options.timeout] 請求超時時間
 * @param  {boolean} [options.notice] 請求失敗是否顯示提示,預設true
 * @return {object}                   promise物件
 */
function request (options = {}) {
  let {
    url,
    method,
    data,
    timeout,
    headers,
    type,
    notice
  } = options

  method = method || `post`
  data = data || {}
  type = type || `t`
  timeout = timeout || config.timeout
  headers = Object.assign({}, config.headers, headers)
  notice = notice === undefined ? true : notice

  let result = {}
  if (method === `get`) {
    result = new Promise((resolve, reject) => {
      axios({
        method: `get`,
        url: fixURL(url, type),
        params: data,
        timeout,
        headers
      })
        .then((res) => {
          checkResponse(res.data, notice).then((data) => {
            resolve(data)
          })
            .catch((data) => {
              reject(data)
            })
        })
        .catch((data) => {
          reject(data)
        })
    })
  } else if (method === `post`) {
    result = new Promise((resolve, reject) => {
      axios({
        method: `post`,
        url: fixURL(url, type),
        data: headers[`Content-Type`] === `application/x-www-form-urlencoded` ? qs.stringify(data) : data,
        timeout,
        headers
      })
        .then((res) => {
          checkResponse(res.data, notice).then((data) => {
            resolve(data)
          })
            .catch((data) => {
              reject(data)
            })
        })
        .catch((data) => {
          reject(data)
        })
    })
  }
  return result
}

export default request

附 Promise 程式碼實現

// 判斷變數否為function
const isFunction = variable => typeof variable === `function`
// 定義Promise的三種狀態常量
const PENDING = `PENDING`
const FULFILLED = `FULFILLED`
const REJECTED = `REJECTED`

class MyPromise {
  constructor (handle) {
    if (!isFunction(handle)) {
      throw new Error(`MyPromise must accept a function as a parameter`)
    }
    // 新增狀態
    this._status = PENDING
    // 新增狀態
    this._value = undefined
    // 新增成功回撥函式佇列
    this._fulfilledQueues = []
    // 新增失敗回撥函式佇列
    this._rejectedQueues = []
    // 執行handle
    try {
      handle(this._resolve.bind(this), this._reject.bind(this)) 
    } catch (err) {
      this._reject(err)
    }
  }
  // 新增resovle時執行的函式
  _resolve (val) {
    const run = () => {
      if (this._status !== PENDING) return
      // 依次執行成功佇列中的函式,並清空佇列
      const runFulfilled = (value) => {
        let cb;
        while (cb = this._fulfilledQueues.shift()) {
          cb(value)
        }
      }
      // 依次執行失敗佇列中的函式,並清空佇列
      const runRejected = (error) => {
        let cb;
        while (cb = this._rejectedQueues.shift()) {
          cb(error)
        }
      }
      /* 如果resolve的引數為Promise物件,則必須等待該Promise物件狀態改變後,
        當前Promsie的狀態才會改變,且狀態取決於引數Promsie物件的狀態
      */
      if (val instanceof MyPromise) {
        val.then(value => {
          this._value = value
          this._status = FULFILLED
          runFulfilled(value)
        }, err => {
          this._value = err
          this._status = REJECTED
          runRejected(err)
        })
      } else {
        this._value = val
        this._status = FULFILLED
        runFulfilled(val)
      }
    }
    // 為了支援同步的Promise,這裡採用非同步呼叫
    setTimeout(run, 0)
  }
  // 新增reject時執行的函式
  _reject (err) { 
    if (this._status !== PENDING) return
    // 依次執行失敗佇列中的函式,並清空佇列
    const run = () => {
      this._status = REJECTED
      this._value = err
      let cb;
      while (cb = this._rejectedQueues.shift()) {
        cb(err)
      }
    }
    // 為了支援同步的Promise,這裡採用非同步呼叫
    setTimeout(run, 0)
  }
  // 新增then方法
  then (onFulfilled, onRejected) {
    const { _value, _status } = this
    // 返回一個新的Promise物件
    return new MyPromise((onFulfilledNext, onRejectedNext) => {
      // 封裝一個成功時執行的函式
      let fulfilled = value => {
        try {
          if (!isFunction(onFulfilled)) {
            onFulfilledNext(value)
          } else {
            let res =  onFulfilled(value);
            if (res instanceof MyPromise) {
              // 如果當前回撥函式返回MyPromise物件,必須等待其狀態改變後在執行下一個回撥
              res.then(onFulfilledNext, onRejectedNext)
            } else {
              //否則會將返回結果直接作為引數,傳入下一個then的回撥函式,並立即執行下一個then的回撥函式
              onFulfilledNext(res)
            }
          }
        } catch (err) {
          // 如果函式執行出錯,新的Promise物件的狀態為失敗
          onRejectedNext(err)
        }
      }
      // 封裝一個失敗時執行的函式
      let rejected = error => {
        try {
          if (!isFunction(onRejected)) {
            onRejectedNext(error)
          } else {
              let res = onRejected(error);
              if (res instanceof MyPromise) {
                // 如果當前回撥函式返回MyPromise物件,必須等待其狀態改變後在執行下一個回撥
                res.then(onFulfilledNext, onRejectedNext)
              } else {
                //否則會將返回結果直接作為引數,傳入下一個then的回撥函式,並立即執行下一個then的回撥函式
                onFulfilledNext(res)
              }
          }
        } catch (err) {
          // 如果函式執行出錯,新的Promise物件的狀態為失敗
          onRejectedNext(err)
        }
      }
      switch (_status) {
        // 當狀態為pending時,將then方法回撥函式加入執行佇列等待執行
        case PENDING:
          this._fulfilledQueues.push(fulfilled)
          this._rejectedQueues.push(rejected)
          break
        // 當狀態已經改變時,立即執行對應的回撥函式
        case FULFILLED:
          fulfilled(_value)
          break
        case REJECTED:
          rejected(_value)
          break
      }
    })
  }
  // 新增catch方法
  catch (onRejected) {
    return this.then(undefined, onRejected)
  }
  // 新增靜態resolve方法
  static resolve (value) {
    // 如果引數是MyPromise例項,直接返回這個例項
    if (value instanceof MyPromise) return value
    return new MyPromise(resolve => resolve(value))
  }
  // 新增靜態reject方法
  static reject (value) {
    return new MyPromise((resolve ,reject) => reject(value))
  }
  // 新增靜態all方法
  static all (list) {
    return new MyPromise((resolve, reject) => {
      /**
       * 返回值的集合
       */
      let values = []
      let count = 0
      for (let [i, p] of list.entries()) {
        // 陣列引數如果不是MyPromise例項,先呼叫MyPromise.resolve
        this.resolve(p).then(res => {
          values[i] = res
          count++
          // 所有狀態都變成fulfilled時返回的MyPromise狀態就變成fulfilled
          if (count === list.length) resolve(values)
        }, err => {
          // 有一個被rejected時返回的MyPromise狀態就變成rejected
          reject(err)
        })
      }
    })
  }
  // 新增靜態race方法
  static race (list) {
    return new MyPromise((resolve, reject) => {
      for (let p of list) {
        // 只要有一個例項率先改變狀態,新的MyPromise的狀態就跟著改變
        this.resolve(p).then(res => {
          resolve(res)
        }, err => {
          reject(err)
        })
      }
    })
  }
  finally (cb) {
    return this.then(
      value  => MyPromise.resolve(cb()).then(() => value),
      reason => MyPromise.resolve(cb()).then(() => { throw reason })
    );
  }
}

參考文章
Promise 官網
ECMAScript 6 入門
Promise 原始碼詳解
Promise 實現原理(附原始碼)
es6-promise-try npm
JavaScript Promise:簡介