手寫系列-這一次,完全搞懂 Promise

這是我參與更文挑戰的第5天,活動詳情查看: 更文挑戰javascript

1、前言

想要實現 Promise,必須先了解 Promise 是什麼,以及 Promise 有哪些功能。java

還不是特別瞭解 Promise 的同窗,建議先移步 ES6入門-Promise 熟悉。git

Promise 是基於 Promises/A+ 規範 實現的,換句話說,咱們能夠按照 Promises/A+ 規範 來手寫 Promise。es6

1.1 小例子

Promise,直譯過來就是承諾,Promise 到底承諾了什麼呢?github

當我在麥當勞點一份漢堡套餐,收銀員會給我一張收據,這個收據就是 Promise,表明我已經付過錢了,麥當勞會爲我作一個漢堡套餐的承諾,我要經過收據來取這個漢堡套餐。web

那麼這個買漢堡獲得的承諾會有如下 3 種狀態:面試

  1. 等待狀態:我剛下單,漢堡還沒作好,這時我能夠在等待漢堡時,同時作其餘事情;
  2. 成功狀態:漢堡作好了,通知我取餐;
  3. 失敗狀態:發現賣完了,通知我退款;

須要注意的是,狀態的修改是不可逆的,當漢堡作好了,承諾兌現了,就不能再回到等待狀態了。npm

總結一下,Promise 就是一個承諾,承諾會給你一個處理結果,多是成功的,多是失敗的,而返回結果以前,你能夠同時作其餘事情。數組

2、Promises/A+

接下來,按照 Promises/A+ 規範 一步步實現 Promise。promise

1. Promise 基本用法

先瞅一眼 ES6 Promise 基本用法。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 異步操做成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(data => {
    console.log('請求成功')
}, err => {
    console.log('請求失敗')
})
複製代碼

1.1 Promise 狀態

Promise 擁有本身的狀態,初始狀態->成功狀態時,執行成功回調,初始狀態->失敗狀態時,執行失敗回調。

  1. pending:初始狀態,能夠轉換爲 fulfilled 或 rejected 狀態;
  2. fulfilled:成功狀態,轉換到該狀態時必須有成功返回值,且不能再次轉換狀態;
  3. rejected:失敗狀態,轉換到該狀態時必須有錯誤緣由,且不能再次轉換狀態;

經過已知的 Promise 3 種狀態,可定義常量 STATUS 和 MyPromise 狀態 status。

代碼以下:

// Promise 3 種狀態
const STATUS = {
  PENDING: 'pending',
  FULFILLED: 'fulfilled',
  REJECTED: 'rejected'
}

class MyPromise {
    // 初始狀態爲 pending
    status = STATUS.PENDING
}

複製代碼

1.2 執行器

從基本用法可知,Promise 須要接收 1 個執行器函數做爲參數,這個函數帶有 2 個參數。

  1. resolve:把 Promise 狀態改爲成功;
  2. reject:把 Promise 狀態改爲失敗;

代碼以下:

class MyPromise {
  constructor (executor) {
      // 執行器
      executor(this.resolve, this.reject)
  }
  // 成功返回值
  value = null
  
  // 失敗返回值
  reason = null
  
  // 修改 Promise 狀態,並定義成功返回值
  resolve = value => {
      if (this.status === STATUS.PENDING) {
          this.status = STATUS.FULFILLED
          this.value = value
      }
  }
  
  // 修改 Promise 狀態,並定義失敗返回值
  reject = () => {
      if (this.status === STATUS.PENDING) {
              this.status = STATUS.REJECTED
              this.reason = value
          }
      }
}
}
複製代碼

1.3 then

Promise 擁有 then 方法,then 方法第一個參數是成功狀態的回調函數 onFulfilled,第二個參數是失敗狀態的回調函數 onRejected。

promise.then(onFulfilled, onRejected)
複製代碼

onFulfilled 要求以下:

  • 必須在 promise 狀態爲完成時調用它,並將 promise 的 value 做爲它的第一個參數;
  • 在 promise 完成以前不能調用它;
  • 它不能被屢次調用;

onRejected 要求以下:

  • 必須在 promise 被拒絕後調用它,以 promise.reason 做爲它的第一個參數;
  • 在 promise 被拒絕以前不能調用它;
  • 它不能被屢次調用;

代碼以下:

class MyPromise {
    then = function (onFulfilled, onRejected) {
      if (this.status === STATUS.FULFILLED) {
          onFulfilled(this.value)
      } else if (this.status === STATUS.REJECTED) {
          onRejected(this.reason)
      }
    }
}
複製代碼

1.4 試試看

按照 Promise 的基本用法,建立 MyPromise 實例 mypromise。

const mypromise = new MyPromise((resolve, reject) => {
  resolve('成功')
})

mypromise.then(data => {
  console.log(data, '請求成功') // 成功打印「成功 請求成功」
}, err => {
  console.log(err, '請求失敗')
})
複製代碼

試行成功,打印結果爲「成功 請求成功」。

源碼地址:基本用法源碼

2. Promise.then

下文將按照 Promises/A+ 規範 完善 MyPromise.then 方法。

Promises/A+ 規範 中標明 then 有如下要求:

1. 可選參數

onFulfilled、onRejected 是可選參數。

  • 若是 onFulfilled 不是函數,則必須忽略它;
  • 若是 onRejected 不是函數,則必須忽略它;

代碼以下:

class MyPromise {
    then(onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
        onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error }
    }
}
複製代碼

2. 屢次調用 then

then 能夠在同一個承諾上屢次調用。

  • 當 promise 完成,全部相應的 onFulfilled 回調必須按照它們的原始調用的順序執行 then;
  • 當 promise 被拒絕,全部相應的 onRejected 回調必須按照它們對 的原始調用的順序執行 then;

2.1 數組緩存回調

能夠理解爲將 onFulfilled、onRejected 做爲數組存儲在 MyPromise 中,而後按照順序執行。

代碼以下:

class MyPromise {
  // 成功回調
  onFulfilledCallback = []

  // 失敗回調
  onRejectedCallback = []

  // 修改 Promise 狀態,並定義成功返回值
  resolve = value => {
    if (this.status === STATUS.PENDING) {
        this.status = STATUS.FULFILLED
        this.value = value

        while(this.onFulfilledCallback.length) {
            this.onFulfilledCallback.shift()(value)
        }
    }
  }
  
  // 修改 Promise 狀態,並定義失敗返回值
    reject = value => {
        if (this.status === STATUS.PENDING) {
            this.status = STATUS.REJECTED
            this.reason = value

            while(this.onRejectedCallback.length) {
                this.onRejectedCallback.shift()(value)
            }
        }
    }

    then = function (onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
        onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
        if (this.status === STATUS.PENDING) {
          this.onFulfilledCallback.push(onFulfilled)
          this.onRejectedCallback.push(onRejected)
        } else if (this.status === STATUS.FULFILLED) {
            onFulfilled(this.value)
        } else if (this.status === STATUS.REJECTED) {
            onRejected(this.reason)
        }
    }
}
複製代碼

由此,咱們已實現了一個基礎的 Promise。

2.2 試試看

看了這麼久,試一試 MyPromise 是否符合要求吧。

代碼以下:

const mypromise = new MyPromise((resolve, reject) => {
  resolve('成功')
})

mypromise.then(data => {
  console.log(data, '1')
})

mypromise.then(data => {
  console.log(data, '2')
})
複製代碼

輸出結果如圖:

image.png

由圖可知,和預期同樣。

源碼地址:屢次調用then 源碼

3. 鏈式調用 then

then 必須返回一個 Promise 來支持鏈式調用 Promise。

示例代碼以下:

mypromise.then(data => {
  console.log(data, '請求成功')
  return '2'
}).then(data => {
  console.log(data, '請求成功')
  return '3'
})
複製代碼

3.1 改寫 then 方法

改動點以下:

  • then 方法須要返回 MyPromise 實例;
  • then 內部調用回調時,需經過 resolvePromise 方法判斷返回值 x 的類型來處理返回值。
class MyPromise {
    then = function (onFulfilled, onRejected) {
        // 返回 MyPromise實例
      const promise2 = new MyPromise((resolve, reject) => {
        if (this.status === STATUS.PENDING) {
            this.onFulfilledCallback.push(() => {
                const x = onFulfilled(this.value)
                resolvePromise(promise2, x, resolve, reject)
            })
            this.onRejectedCallback.push(() => {
                const x = onRejected(this.value)
                resolvePromise(promise2, x, resolve, reject)
            })
        } else if (this.status === STATUS.FULFILLED) {
            const x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
        } else if (this.status === STATUS.REJECTED) {
            const x = onRejected(this.error)
            resolvePromise(promise2, x, resolve, reject)
        }
      }) 

      return promise2
    }
}
複製代碼

上述代碼引用了 resolvePromise 來處理 Promise.then 的返回值,

3.2 resolvePromise

Promises/A+ 規範 對resolvePromise 的要求以下:

image.png

  • 若是 promise2 === x, 執行 reject,錯誤緣由爲 TypeError
  • 若是 x 是函數或對象
    • 若是 x.then 是函數
      • 執行 x.then
    • 若是 x.then 不是函數
      • 執行 resolve(x)
  • 若是 x 不是函數或對象
    • 執行 resolve(x)

代碼以下:

function resolvePromise (promise2, x, resolve, reject) {
  // 若是 promise2 === x, 執行 reject,錯誤緣由爲 TypeError
    if (promise2 === x) {
      reject(new TypeError('The promise and the return value are the same'))
    }

    // 若是 x 是函數或對象
    if (typeof x === 'object' || typeof x === 'function') {
      let then
      try {
        then = x.then
      } catch (error) {
        reject(error)
      }

      // 若是 x.then 是函數
      if (typeof then === 'function') {
        then.call(x, y => {
          // resolve的結果依舊是promise 那就繼續解析
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          reject(err);// 失敗了
        })
      } else {
          // 若是 x.then 不是函數
          resolve(x)
      }
    } else {
        // 若是 x 不是 promise 實例
        resolve(x)
    }
}
複製代碼

3.3 試一試

試試看能不能符合預期,鏈式調用 then 吧。

const mypromise = new MyPromise((resolve, reject) => {
  resolve('成功')
})

const mypromise2 = new MyPromise((resolve, reject) => {
  resolve('成功2')
})

mypromise.then(data => {
  console.log(data, '1')
  return mypromise2 
}).then(data => {
  console.log(data, '2')
})
複製代碼

輸出結果爲:

image.png

成功符合預期!

源碼地址:鏈式調用then源碼

4. 異步事件

Promises/A+ 規範 要求 onFulfilled、onRejected 在執行上下文堆棧以前不得調用。

4.1 事件隊列

當遇到一個異步事件後,並不會一直等待異步事件返回結果,而是會將這個事件掛在與執行棧不一樣的隊列中,咱們稱之爲事件隊列。

當全部同步任務執行完成後,系統纔會讀取"事件隊列"。

事件隊列中的事件分爲宏任務和微任務:

  1. 宏任務:瀏覽器/Node發起的任務,如 window.setTimeout;
  2. 微任務:Js 自身發起的,如 Promise;

事件隊列就是先執行微任務,再執行宏任務,而宏任務和微任務包含如下事件:

宏任務 微任務
setTimeout Promise
setInterval queueMicrotask
script(總體代碼塊) -

看看下面這個例子,你知道答案嗎?

setTimeout(function () {
  console.log(1);
});
new Promise(function(resolve,reject){
  console.log(2)
  resolve(3)
}).then(function(val){
  console.log(val);
})
console.log(4);
複製代碼

打印結果的順序是2->4->3->1。事件隊列以下:

  1. 主隊列,同步任務,new Promise 內部的同步任務
new Promise(function(resolve,reject){
  console.log(2)
  })
複製代碼
  1. 主隊列,同步任務,new Promise 後的 console.log(4)
console.log(4)
複製代碼
  1. 異步任務的微任務
promise.then(function(val){
  console.log(val);
})
複製代碼
  1. 異步任務的宏任務
setTimeout(function () {
  console.log(1);
});

複製代碼

所以,想要實現 onFulfilled、onRejected 在執行上下文堆棧以前不得調用,咱們須要把 onFulfilled、onRejected 改形成微任務,這裏使用 queueMicrotask 來模擬實現微任務,代碼以下:

class MyPromise {
     then (onFulfilled, onRejected) {
        const realOnFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
        const realOnRejected = typeof onRejected === 'function' ? onRejected : error => { throw error }
      
        const promise2 = new MyPromise((resolve, reject) => {
          
          const fulfilledMicrotask = () =>  {
            // 建立一個微任務等待 promise2 完成初始化
            queueMicrotask(() => {
              try {
                // 獲取成功回調函數的執行結果
                const x = realOnFulfilled(this.value);
                // 傳入 resolvePromise 集中處理
                resolvePromise(promise2, x, resolve, reject);
              } catch (error) {
                reject(error)
              } 
            })  
          }
  
        const rejectedMicrotask = () => { 
            // 建立一個微任務等待 promise2 完成初始化
            queueMicrotask(() => {
              try {
                // 調用失敗回調,而且把緣由返回
                const x = realOnRejected(this.reason);
                // 傳入 resolvePromise 集中處理
                resolvePromise(promise2, x, resolve, reject);
              } catch (error) {
                reject(error)
              } 
            }) 
          }

          if (this.status === STATUS.PENDING) {
              this.onFulfilledCallbacks.push(fulfilledMicrotask)
              this.onRejectedCallbacks.push(rejectedMicrotask)
          } else if (this.status === STATUS.FULFILLED) {
            fulfilledMicrotask()
          } else if (this.status === STATUS.REJECTED) {
            rejectedMicrotask()
          }
        }) 

        return promise2
    }
}
複製代碼

下面試試能不能成功?

4.2 試試看

const mypromise = new MyPromise((resolve, reject) => {
  setTimeout(() => resolve('成功'), 1000)
})

const mypromise2 = new MyPromise((resolve, reject) => {
  setTimeout(() => resolve('成功2'), 1000)
})

mypromise.then(data => {
  console.log(data, '1')
  return mypromise2 
}).then(data => {
  console.log(data, '2')
})

複製代碼

打印結果如圖:

image.png

成功按順序打印。

源碼地址:異步事件源碼

3. Promise/A+ 測試

下面將用 Promise/A+ 測試工具 promises-aplus-tests 測試咱們手寫的 Promise 是否符合規範。

3.1 安裝 promises-aplus-tests

npm install promises-aplus-tests -D
複製代碼

3.2 爲 MyPromise 添加 deferred

MyPromise {
  ......
}

MyPromise.deferred = function () {
  var result = {};
  result.promise = new MyPromise(function (resolve, reject) {
    result.resolve = resolve;
    result.reject = reject;
  });

  return result;
}
module.exports = MyPromise;

複製代碼

3.3 配置啓動命令

"scripts": {
    "test:promise": "promises-aplus-tests ./src/手寫系列/Promise/testPromise"
  },
複製代碼

3.4 開始測試

npm run test:promise
複製代碼

哇哦,所有成功!!

image.png

源碼地址:testPromise.js 源碼

3、總結

以上,咱們實現了一個符合 Promises/A+ 規範 的 Promise,咱們能夠繼續本身動手,參考 ES6 的 Promise 方法對 MyPromise 進行拓展練習。

總結一下 Promise 其實就是一個幫助咱們執行異步任務的對象,由於 Javascript 單線程的特性,致使必須經過爲異步任務添加回調來獲得異步任務的結果。爲了解決回調地獄,Promise 應運而生。

Promise 經過對異步任務執行狀態的處理,讓咱們能夠在 Promise.then 中獲取任務結果,讓代碼更加清晰優雅。

Promise.then 的鏈式調用,以順序的方式來表達異步流,讓咱們更好的維護異步代碼。

可經過 github源碼 進行實操練習。

但願能對你有所幫助,感謝閱讀~別忘了點個贊鼓勵一下我哦,筆芯❤️

相關文章
相關標籤/搜索