談談你對Promise的理解

fus發表於2019-05-24

背景

Promise是為了解決Javascript回撥巢狀過多導致回撥地獄(callbackhell)而產生的。目前已被納入了es2015規範,主流瀏覽器都支援Promise。為了在工作中更好的運用Promise,我們需要理解Promise規範與內部實現機制,下面我們來手動實現一個Promise。

Promise/A+規範

在寫程式碼之前讓我們先了解下 Promise/A+規範。
一個promise有三種狀態:

  • pending:表示初始狀態,可以轉移到 fullfilled 或者 rejected 狀態
  • fulfilled:表示操作成功,不可轉移狀態
  • rejected:表示操作失敗,不可轉移狀態
  • 必須有一個 then 非同步執行方法,then 接受兩個引數且必須返回一個promise:

談談你對Promise的理解

借用這張來自MDN的流程圖我們可以清晰的看到 Promise 狀態的流轉過程。

簡單版

下面我們來實現一個簡單版的 Promise:

function Promise1(executor){
  let self = this;
	self.status = 'pending';
  self.value = undefined;
  self.reason = undefined;
  
  function resolve(value) {
  	if(self.status==='pending'){
    	self.status = 'fullfilled';
      self.value = value;
    }
  }
  function reject(reason) {
  	if(self.status==='pending') {
    	self.status = 'rejected';
      self.reason = reason;
    }
  }
  
  try{
  	executor(resolve,reject);
  }catch(e) {
  	reject(e);
  }
}

Promise1.prototype.then = function(onFullfilled,onRejected) {
	if(this.status==='fullfilled') {
  	onFullfilled(this.value);
  }
  if(this.status==='rejected') {
  	onRejected(this.reason);
  }
}
//測試
let p= new Promise1(function(resolve,reject){resolve(1)});
p.then(function(x){console.log(x)})
//輸出1
複製程式碼

支援非同步

現在,我們實現了最簡單的 Promise。以上版本的Promise是存在很多問題的。為什麼呢?最大的問題是它不支援非同步,然而在現實中,Promise絕大多數使用場景都是非同步。讓我們來為 Promise 加入非同步功能。

const PENDING = 'pending';
const FULFILLED = 'fullfilled';
const REJECTED = 'rejected';
function Promise1(executor){
  let self = this;
	self.status = PENDING;
  self.value = undefined;
  self.reason = undefined;
  self.fullfilledCallbacks = [];
  self.rejectedCallbacks = [];

  function resolve(value) {
    if(value instanceof Promise) {
    	value.then(resolve,reject);
    }
  	setTimeout(function(){
    	if(self.status===PENDING){
        self.status = FULFILLED;
        self.value = value;
        self.fullfilledCallbacks.forEach(function(cb){
        	cb(self.value)
        })
      }
    })
  }
  function reject(reason) {
  	setTimeout(function(){
    	if(self.status===PENDING) {
        self.status = REJECTED;
        self.reason = reason;
        self.rejectedCallbacks.forEach(function(cb){
        	cb(self.reason);
        })
      }
    })
  }

  try{
  	executor(resolve,reject);
  }catch(e) {
  	reject(e);
  }
}

Promise1.prototype.then = function(onFulfilled,onRejected) {
  let self = this;
  return new Promise1(function(resolve,reject){
  		function success(value) {
        let _value = (typeof onFulfilled === 'function' && onFulfilled(value)) || value;
        resolve(_value)
      }
      function error(reason) {
        let _reason = (typeof onRejected === 'function' && onRejected(reason)) || reason;
        reject(_reason);
      }
    	if(self.status==PENDING) {
      	self.fullfilledCallbacks.push(success);
        self.rejectedCallbacks.push(error);
      } else if(self.status==FULLFILLED){
      	success.call(this,this.value)
      } else if(self.status==REJECTED) {
      	error.call(this,this.reason);
      }
  })
}

複製程式碼

以上程式碼中,我們做了如下更改:

  1. 將 Promise 三個狀態定義為常量,方便維護
  2. 對於 Promise resolve和reject 函式執行加入非同步處理
  3. 在Promise.then中返回新的Promise物件,使Promise可以支援鏈式呼叫

錯誤處理以及靜態方法

下面讓我們來為Promise 新增錯誤處理以及靜態方法:

//錯誤處理
Promise1.prototype.catch = function(onRejected) {
    return this.then(null, onRejected);
}

//返回fullfilled Promise物件
Promise1.resolve = function (value) {
    return new Promise1(resolve => {
        resolve(value);
    });
}
//返回 rejected Promise 物件
Promise1.reject = function (reason) {
    return new Promise1((resolve, reject) => {
        reject(reason);
    });
}
//Promise.all方法
Promise1.all = function(promises) {
    function gen(length, resolve) {
        let count = 0;
        let values = [];
        return function(i, value) {
            values[i] = value;
            if (++count === length) {
                resolve(values);
            }
        }
    }
    return new Promise1((resolve, reject) => {
        let done = gen(promises.length, resolve);
        promises.forEach((promise, index) => {
            promise.then((value) => {
                done(index, value)
            }, reject)
        })
    })
}
//Promise.race方法
Promise1.race = function(promises) {
    return new Promise1((resolve, reject) => {
        promises.forEach((promise, index) => {
           promise.then(resolve, reject);
        });
    });
}

複製程式碼

這裡有個問題,就是在當我們console.log(Promise1.resolve('a'))的時候,我發現列印出來的狀態竟然是 pending狀態,我猜想原因是應該是resolve中函式非同步執行,在當我們console的時候setTimeout中程式碼未執行,所以我給出的解決方法是將狀態變化與賦值移到setTimeout外面,這樣就不會產生剛才的問題了,更改後程式碼長這樣:

function resolve(value) {
    if(value instanceof Promise) {
    	value.then(resolve,reject);
    }
  	self.status = FULFILLED;
  	self.value = value;
  	setTimeout(function(){
    	if(self.status===PENDING){
        self.fullfilledCallbacks.forEach(function(cb){
        	cb(self.value)
        })
      }
    })
  }

function reject(reason) {
    self.status = REJECTED;
    self.reason = reason;
  	setTimeout(function(){
    	if(self.status===PENDING) {
        self.rejectedCallbacks.forEach(function(cb){
        	cb(self.reason);
        })
      }
    })
  }
複製程式碼

總結

經過以上實踐,我們成功的手寫了一個功能完備的 Promise。這裡給我的最大啟發是如果我們想學習一個東西,必須深入到它的底層,瞭解它的執行原理與具體實現方法,並且可以造一個簡單的輪子,這樣才算我們掌握了該知識點。從前的我對於這一點沒有關注的太多,導致在用某個知識點時只是掌握的它的表層用法,在高階一點的使用場景時完全不會運用。以後我會更加註重原始碼方面的學習,彌補我這方面的不足。

相關文章