Promise 原理解析與源碼實現(遵循 Promise/A+ 規範)

--文末附視頻教程+源碼javascript

1. 構造函數

new Promise 時,須要傳遞一個 executor 執行器,執行器馬上執行(同步執行),executor 接受兩個參數,分別是 resolve(成功) 和 reject(失敗)。java

promise 有 3 個狀態:git

- pending:初始狀態,既不是成功,也不是失敗狀態。
- fulfilled:成功狀態,意味着操做成功完成。
- rejected:失敗狀態,意味着操做失敗。
複製代碼
const PENDING = Symbol('PENDING')
const RESOLVED = Symbol('RESOLVED')
const REJECTED = Symbol('REJECTED')

// Promise 構造函數
function Promise (executor) {
  // 當前的狀態,默認是 pending
  this.status = PENDING
  // 保存回調函數,由於 then 能夠調用屢次,因此以數組保存
  this.onResolvedCallbacks = []
  this.onRejectedCallbacks = []
  // 成功值
  this.value = undefined
  // 拒絕的緣由
  this.reason = undefined
  // resolve、reject 是用來改變狀態,
  // 而且根據 then 方法註冊回調函數的順序依次調用回調函數
  // resolve 是執行成功後調用的函數
  const resolve = (value) => {
    // 若是狀態不是 pending,說明狀態已經改變,不能再發生變化
    if (this.status === PENDING) {
      this.value = value
      this.status = RESOLVED
      this.onResolvedCallbacks.forEach(fn => fn())
    }
  }
  // reject 是執行失敗後調用的函數
  const reject = (reason) => {
    if (this.status === PENDING) {
      this.reason = reason
      this.status = REJECTED
      this.onRejectedCallbacks.forEach(fn => fn())
    }
  }
  // 使用 try...catch... 捕捉代碼執行過程當中可能拋出的異常
  try {
    // 執行器默認會當即執行
    executor(resolve, reject)
  } catch(e) {
    // 若是執行時發生錯誤(包括手動拋出的異常),等同於執行失敗
    reject(e)
  }
}
複製代碼

2. then 方法實現

實現 Promise 的 then 方法,then 方法有兩個可選參數,onFulfilledonRejected,而且必須返回一個 promise 對象。 若是 onFulfilledonRejected 返回的是一個 promise,會自動執行這個 promise,並採用它的狀態。若是成功則將成功的結果向外層的下一個 then 傳遞。es6

Promise.prototype.then = function(onFulfilled, onRejected) {
  // onFulfilled 和 onRejected 是可選的,這裏須要對不傳的時候作兼容處理
  // onFulfilled 若是不是函數,就構建一個函數,函數直接返回結果。
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
  // onRejected 若是不是函數,就構建一個函數,函數直接拋出異常。
  onRejected = typeof onRejected === 'function' ? onRejected : reason => {
    throw reason;
  }

  let promise2 = new Promise((resolve, reject) => {
    // 狀態爲 resolved 或 rejected 時,主要是 new Promise 時執行器裏面調用 resolve/reject 是同步的
    if (this.status === RESOLVED) {
      // 使用 setTimeout (宏任務),確保 onFulfilled 和 onRejected 方法異步執行,也確保 promise2 已經定義,
      // 若是不使用 setTimeout,會致使執行 resolvePromise(promise2, x, resolve, reject) 時 promise2 未定義而報錯。
      setTimeout(() => {
        // try...catch... 捕捉代碼錯誤或手動拋出的異常,報錯或異常看成執行失敗處理。異步代碼的報錯沒法被外層的 try...catch... 捕獲
        try {
          const x = onFulfilled(this.value)
          // x 多是 promise 也多是普通值,x 本次 then 調用中 onFulfilled 或 onRejected 回調函數返回的結果,須要傳遞給下一個 then 的回調函數
          // 使用公共方法 resolvePromise 處理不一樣狀況,並實現 x 值的傳遞。
          resolvePromise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      }, 0)

      return
    }
    if (this.status === REJECTED) {
      setTimeout(() => {
        try {
          const x = onRejected(this.reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      }, 0)
    }
    // 狀態爲 pending 時,主要是 new Promise 時執行器裏面調用 resolve/reject 是異步的
    if (this.status === PENDING) {
      // 由於是異步的,不知道什麼時候執行完成,因此這裏先存好回調函數的調用(訂閱),等狀態改變後再執行(發佈)
      this.onResolvedCallbacks.push(() => {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }

        }, 0)
      })
      this.onRejectedCallbacks.push(() => {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      })
    }
  })
  return promise2
}
複製代碼

有部分同窗可能會認爲 then 是在 promise 狀態改變後(即有返回值後)才執行,其實 then 是當即執行,是 onFulfilledonRejected 纔在狀態改變後執行。github

3. Promise Resolution Procedure 的實現

Promise 解決程序(promise resolution procedure) 是一個抽象的操做,須要輸入一個 promise 和一個值,咱們表示爲[[Resolve]](promise, x)shell

這裏咱們定義公用方法 resolvePromise 來實現這個過程。npm

resolvePromise 主要實現的功能是:json

  1. 判斷 promise2x 是否指向同一對象,若是是 promise2 執行失敗而且使用 TypeError 做爲執行失敗的緣由。數組

    例如:promise

    const p = new Promise((resolve, reject) => resolve(1))
    let promise2 = p.then(() => {
      // x
      return promise2
    })
    複製代碼
  2. 判斷 x 是否是一個 promise 對象,若是是就經過調用 resolve/reject 獲取狀態並向下個 then 傳遞 。

  3. 若是 x 是一個普通對象/值,則直接將 x 做爲結果值向下個 then 傳遞。

下面是代碼實現:

const resolvePromise = (promise2, x, resolve, reject) => {
  // 若是 promise2 和 x 指向同一對象, promise2 執行失敗而且使用 TypeError 做爲執行失敗的緣由
	if (promise2 === x) {
		return reject(new TypeError('Chaining cycle detected for promise #<promise>'))
	}
	if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
		// 防止屢次調用成功或者失敗
		let called;
		try {
      // 首先存儲一個指向 x.then 的引用,而後測試並調用該引用,以免屢次訪問 x.then 屬性
      // 預防取 x.then 的時候錯誤,例如: .then 是經過 Object.defineProperty 定義的,定義的 get() {}(getter) 可能代碼錯誤或拋出異常
      let then = x.then
      // 沒用 x.then 判斷由於怕再次取 .then 的時候出錯。例如:經過 Object.defineProperty 定義的 then 可能第一次調用不報錯,第二次調用報錯或屢次調用返回的值可能不一樣
			if (typeof then === 'function') {
        // 若是 then 是一個函數,則認爲 x 是一個 promise,以 x 爲 它的 this 調用它, then 調用完成就會取到 x 的狀態,採用 x 的狀態返回
        // 而且傳遞兩個回調函數做爲參數,第一個參數是 resolvePromise,第二個參數是 rejectPromise
				then.call(x, y => { 
					if (called) {
						return
					}
					called = true
          // y 是 x 調用 then 後成功的結果,採用這個結果
          // y 可能仍是一個 promise,因此進行遞歸調用,直到結果是一個普通值
					resolvePromise(promise2, y, resolve, reject)
				}, r => {
					// r 是調用 x.then 後報錯或異常,再也不判斷是不是 promise,直接傳遞
					if (called) {
						return
					}
					called = true
					reject(r); // 失敗結果向下傳遞
				});
			} else {
        // 普通對象,直接傳遞給下一個 then
				resolve(x)
			}
		} catch (e) {
      // 發生代碼錯誤或手動拋出異常,則當執行失敗處理並以 e 爲失敗緣由
			if (called) {
				return
			}
			called = true
			reject(e)
		}
	} else {
    // 普通值,直接傳遞給下一個 then
		resolve(x)
	}
}
複製代碼

4. deferred 的實現

Promise.defer = Promise.deferred = function () {
  let dfd = {}
  dfd.promise = new Promise((resolve, reject) => {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd
}
複製代碼

deferred 的做用:

  1. 使用 promise-aplus-test 工具須要用到這個方法

  2. 這個方法能夠減小代碼嵌套

    例如:

    const Promise = require('./pormise')
    const fs = require('fs')
    const readfile = url => {
      return new Promise((resolve, reject) => { // 一層嵌套
        fs.readFile(url, 'utf-8', (err, data) => { // 二層嵌套
          if(err) reject(err)
          resolve(data)
        })
      })
    }
    readfile('./package.json').then(data => console.log(data))
    複製代碼

    使用 deferred

    const readfile = url => {
      let dfd = Promise.defer()
      // 減小了一層嵌套
      fs.readFile(url, 'utf-8', (err, data) => {
        if(err) dfd.reject(err)
        dfd.resolve(data)
      })
      return dfd.promise
    }
    複製代碼

5. 測試

測試使用工具 promises-aplus-test

安裝:npm install -g promises-aplus-test

測試:promise-aplus-test promise.js

使用本文提供的 github源碼 則能夠直接運行如下命令:

// 安裝依賴工具
npm install
// 運行測試指令
npm run test
複製代碼

6. Promise的其餘方法

上面已經實現了 Promise 的核心部分代碼,但原生的 Promise 還提供一些其餘的方法。

  1. Promise.resolve()
  2. Promise.reject()
  3. Promise.all()
  4. Promise.race()
  5. Promise.prototype.catch()
  6. Promise.prototype.finally()

1)Promise.resolve()

有時須要將現有對象轉爲 Promise 對象,Promise.resolve()方法就起到這個做用。

Promise.resolve()等價於下面的寫法。

Promise.resolve('foo')
// 等價於
new Promise(resolve => resolve('foo'))
複製代碼

Promise.resolve方法的參數分紅四種狀況。

  • 參數是一個promise,Promise.resolve 不作任何修改,原封不動返回
  • 參數是一個 thenable 對象,Promise.resolve方法會將這個對象轉爲 Promise 對象,而後就當即執行thenable對象的then方法。
  • 參數不是具備 then 方法的對象,或根本就不是對象,Promise.resolve方法返回一個新的 Promise 對象,狀態爲resolved
  • 不帶有任何參數,直接返回一個resolved狀態的 Promise 對象。
Promise.resolve = function (param) {
        if (param instanceof Promise) {
        return param;
    }
    return new Promise((resolve, reject) => {
        if (param && param.then && typeof param.then === 'function') {
            setTimeout(() => {
                param.then(resolve, reject);
            });
        } else {
            resolve(param);
        }
    });
}
複製代碼

2)Promise.reject()

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

Promise.reject()方法的參數,會原封不動地做爲reject的理由,變成後續方法的參數。這一點與Promise.resolve方法不一致。

Promise.reject = function (reason) {
    return new Promise((resolve, reject) => {
        reject(reason);
    });
}
複製代碼

3)Promise.all()

Promise.all()方法用於將多個 Promise 實例,包裝成一個新的 Promise 實例。

const p = Promise.all([p1, p2, p3]);
複製代碼

上面代碼中,Promise.all()方法接受一個數組做爲參數,p1p2p3都是 Promise 實例,若是不是,就會先調用下面講到的Promise.resolve方法,將參數轉爲 Promise 實例,再進一步處理。另外,Promise.all()方法的參數能夠不是數組,但必須具備 Iterator 接口,且返回的每一個成員都是 Promise 實例。

p的狀態由p1p2p3決定,分紅兩種狀況。

(1)只有p1p2p3的狀態都變成fulfilledp的狀態纔會變成fulfilled,此時p1p2p3的返回值組成一個數組,傳遞給p的回調函數。

(2)只要p1p2p3之中有一個被rejectedp的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。

Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    // 存放結果,.all 傳入的參數是數組,返回結果也是數據
    let result = []
    // 使用計數器,記錄多個異步併發問題
    let index = 0
    if (promises.length === 0) {
      resolve(result)
    } else {
      // 處理返回值
      function processValue(i, data) {
        result[i] = data
        // 計數器記錄的個數等於傳入的數組長度,說明所有認爲已完成,能夠返回結果
        if (++index === promises.length) {
          resolve(result)
        }
      }
      for (let i = 0; i < promises.length; i++) {
        let current = promises[i]
        // 判斷當前的處理對象是 promise 仍是普通值
        if (isPromise(current)) {
          // 取當前的處理對象的執行結果,若是有一個執行失敗,則直接 reject
          current.then(data => {
            processValue(i, data)
          }, reject)
        } else {
          processValue(i, current)
        }
      }
    }
  })
}
複製代碼

4)Promise.race()

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

const p = Promise.race([p1, p2, p3]);
複製代碼

上面代碼中,只要p1p2p3之中有一個實例率先改變狀態,p的狀態就跟着改變。那個率先改變的 Promise 實例的返回值,就傳遞給p的回調函數。

Promise.race()方法的參數與Promise.all()方法同樣,若是不是 Promise 實例,就會先調用下面講到的Promise.resolve()方法,將參數轉爲 Promise 實例,再進一步處理。

Promise.race = function (promises) {
    return new Promise((resolve, reject) => {
        if (promises.length === 0) {
            return;
        } else {
            for (let i = 0; i < promises.length; i++) {
                Promise.resolve(promises[i]).then((data) => {
                    resolve(data);
                    return;
                }, (err) => {
                    reject(err);
                    return;
                });
            }
        }
    });
}
複製代碼

5)Promise.prototype.catch()

Promise.prototype.catch 方法是 .then(null, rejection).then(undefined, rejection) 的別名,用於指定發生錯誤時的回調函數。

Promise.prototype.catch = function (onRejected) {
    return this.then(null, onRejected);
}
複製代碼

6)Promise.prototype.finally()

finally方法用於指定無論 Promise 對象最後狀態如何,都會執行的操做。該方法是 ES2018 引入標準的。

Promise.prototype.finally = function (callback) {
    return this.then((value) => {
        return Promise.resolve(callback()).then(() => {
            return value;
        });
    }, (err) => {
        return Promise.resolve(callback()).then(() => {
            throw err;
        });
    });
}
複製代碼

參考文檔:

Promises/A+ 規範(譯文)

Promises/A+ 規範(官方原文英文版)

阮一峯 - Promise 對象 - ECMAScriptS 6入門

Promise - JavaScript | MDN

獲取視頻教程+源碼

相關文章
相關標籤/搜索