JavaScript進階之手寫Promise

前言

Promise是當前ES6語法中的異步解決方案,本文從基本概念開始,層層分析,一步一步手寫實現Promise,但願能和你們一塊兒完全掌握Promise。 javascript

概述

Promise是異步編程的一種解決方案,跟傳統回調函數來實現異步相比,其設計更合理,代碼更易懂。Promise是"美顏後"的異步解決方案.
在ES6中, 其寫進了語言標準提供原生支持。
Promise有兩大特色:html

  1. 對象的狀態不受外界的影響,Promise對象表明一個異步操做,有三種狀態: pending , fulfilled , rejected
  2. Promise是一個狀態機,一旦狀態改變,就不會再變,而且在任什麼時候候均可以獲得這個結果。

上圖摘自MDNjava

Promise的具體用法能夠參考阮一峯老師的 《ECMAScript 6 入門》MDN
這裏有幾個點須要注意:git

  1. Promise能夠有finally,用於作跟狀態無關的業務邏輯操做。
  2. Promise中定義的函數在resolve以後仍是能夠執行語句,因此建議在resolve前加上return
new Promise((resolve, reject) => {
  resolve(1);//爲了防止輸出2,改爲return resolve,平常編碼中也要養成return resolve的習慣
  console.log(2);// 能夠輸出2
}).then(r => {
  console.log(r);
});
複製代碼
  1. then方法中的第二個參數 reject=>{} 能夠省略,建議使用 catch(error=>{}) ,其不只可以獲取reject的內容,還能夠捕獲到Promise函數和then函數中產生的異常。Promise內部的異常會被吃掉,不會被外部的異常捕獲。
  2. 注意Promise.all和Promise.race的用法及使用場景。

若是熟悉了Promise的使用,其實咱們知道,Promise提供了異步編程的語法糖,使原來異步回調的操做能夠用同步的方式來表達。es6

回調地獄

在傳統AJAX異步解決方案中,咱們通常使用回調函數來解決數據的接收和處理,以下:github

$.get(url, (data) => {
    console.log(data)
)
複製代碼

在某些需求場景下,咱們須要發送多個異步請求,而且每一個請求之間結果之間須要相互依賴,隨着回調函數相互嵌套的增長,函數之間的邏輯就會耦合在一塊兒,難以維護,造成回調地獄。以下所示:面試

let country = 'china';
let city = 'shanghai';
let district = 'PD'
$.get(`xxxxx/countries`,countries=>{
  /** **這裏能夠再第一個select控件中,渲染國家列表, **/
  countries.forEach((item)=>{
  		if(item===country){
        //查找並選中當前國家
        $.get(`xxxxx/findCitiesByCountry/${country}`, cities => {
             /** **這裏能夠再第二個select控件中,渲染城市列表, **/
            cities.forEach((item)=>{
              if(item === city){
               //查找並選中當前城市
               $.get(`xxxxx/findDistrictsByCity/${city}`, dists => {
											  /** **這裏能夠再第三個select控件中,渲染地區列表, **/
                 			dists.forEach(item=>{
                      	if(item==district){
                        	 //查找並選中地區
                        }
                      })
               })
              }
            })
        })
      }
  });
});
複製代碼

上述是一個簡單的三級聯動功能,使用三個回調函數。它們相互嵌套邏輯複雜,耦合嚴重。
Promise解決了回調地獄問題,經過Promise將上述代碼改寫成編程

let country = 'china';
let city = 'shanghai';
let district = 'PD'
new Promise(() => {
    $.get(`xxxxx/countries`, countries => {
        return countries;
    });
}).then((countries) => {
    countries.forEach((item) => {
        if (item === country) {
            $.get(`xxxxx/findCitiesByCountry/${country}`, cities => {
                return cities;
            })
        }
    })
}).then((cities) => {
    cities.forEach((item) => {
        if (item === city) {
            $.get(`xxxxx/findDistrictsByCity/${city}`, dists => {
                return dists;
            })
        }
    })
}).then((dists) => {
    dists.forEach(item => {
        if (item == district) {
            //查找並選中地區
        }
    })
})
複製代碼

此時,將異步執行由原來的回調,改爲了 then...then....then... 鏈式調用的方式。
線性的鏈式執行(同步)更符合人類的思考習慣(更直白的說,按照順序一步一步的閉環符合人類思考習慣。Promise就是將本來異步回調的語法形式,改寫成同步。因此實現Promise,就是把異步回調這種醜陋的方式改爲鏈式調用。經過手寫Promise,咱們來理解和消化其設計思想。 數組

開始

有了上述的鋪墊,咱們瞭解Promise的概念和特徵,也知道了Promise的優點,下面咱們一步步來實現Promise。promise

  1. Promise是一個構造函數,而且傳入的參數是一個函數,而且該函數在構造函數中執行
function Promise(executor){
	try{
    executor()
  }catch(e){
  	console.log(e):
  }
}
複製代碼
  1. executor函數的兩個參數resolvereject,是executor函數中的回調函數。
function Promise(executor){
  function resolve(value){
    //能夠將executor中的數據傳入resolve中
  }
  function reject(value){
    //能夠將executor中的數據傳入reject中
  }
	try{
    executor(resolve,reject)
  }catch(e){
  	console.log(e):
  }
}
複製代碼
  1. Promise實現了狀態機,在執行resolve時,由 PENDING=>FULFILLED ,在執行reject時,由 PENDING=>REJECTED 。
const pending = 'PENDING';
const rejecting = 'REJECTED';
const fulfilled = 'FULFILLED';
function Promise(executor){
  var that = this;
  that.status = pending;
  that.value = null;
  that.error = null;
  function resolve(val){
    //當且僅當PENDING=》FULFILLED
    if(that.status === pending){
      that.status = fulfilled;
    	that.value = val;
    }
  }
  function reject(val){
    //當且僅當PENDING=》REJECTED
    if(that.status === pending){
      that.status = rejecting;
    	that.error = val;
    }
  }
  try{
    executor(resolve,reject);
  }catch(e){
    //在executor中產生的異常在reject中能夠捕獲。可是reject的異常,智能catch捕獲
  	reject(e);
  }
}

Promise.prototype.then = function(onFulfilled, onRejected){
	var that = this;
  if(that.status === fulfilled){
    //當狀態改變後,執行then中的回調
  	onFulfilled(that.value);
  }
  if(that.status === rejecting){
    //同上
    onRejected(that.error)
  }
};
複製代碼

執行以下代碼

new Promise((resolve)=>{
    resolve(1);
  }).then((res)=>{
    console.log(res);
  });
複製代碼

打印結果以下

  1. Promise是異步的

若是executor函數存在異步,則須要等待resolve或者reject回調執行纔會執行then中的函數體。

此處使用回調來解決異步:
第一步:定義兩個Promise的成員:onResolvedCallBack和onRejectCallBack,存儲then函數中的入參。
第二步:當執行then函數時,若是當前狀態仍是PENDING(此時executor內部有異步操做)那麼就將then中的參數傳入onResolvedCallBack和onRejectCallBack中。若是此時狀態是非PENDING,那麼直接執行傳入的函數便可。
第三步:Promise中的resolve函數執行時,觸發onResolvedCallBack或者onRejectCallBack中的函數。

具體代碼以下:

const pending = 'PENDING';
const rejecting = 'REJECTED';
const fulfilled = 'FULFILLED';
function Promise1(executor) {
  var that = this;
  that.status = pending;
  that.value = null;
  that.error = null;
  that.resolvedCallbacks = [];
  that.rejectedCallbacks = [];
  
  function resolve(val) {
    if (that.status === pending) {
      that.status = fulfilled;
      that.value = val;
      that.resolvedCallbacks.map(cb => cb(that.value));
    }
  }
  
  function reject(val) {
    if (that.status === pending) {
      that.status = rejecting;
      that.error = val;
      that.rejectedCallbacks.map(cb => cb(that.value));
    }
  }
  
  try {
    executor(resolve, reject);
  } catch (e) {
    reject(e);
  }
}


Promise1.prototype.then = function (onFulfilled, onRejected) {
  var that = this;
  //爲了保證兼容性,then的參數只能是函數,若是不是要防止then的穿透問題
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : r => {
          throw r
        }
  if (that.status === pending) {
    that.resolvedCallbacks.push(onFulfilled)
    that.rejectedCallbacks.push(onRejected)
  }
  
  if (that.status === fulfilled) {
    onFulfilled(that.value);
  }
  
  if (that.status === rejecting) {
    onRejected(that.error)
  }

};
複製代碼

執行以下一段代碼:

let p = new Promise((resolve) => {
   setTimeout(() => {
      resolve(1);
   }, 3000)
});

p.then((res) => { console.log(res); });

console.log(2);
複製代碼

打印結果以下:

image.png

咱們再來執行以下代碼

let p = new Promise((resolve) => {
  resolve(1);
});

p.then((res) => { console.log(res); });

console.log(2);
複製代碼

此處咱們打印結果爲

image.png

可是真正的Promise打印結果爲:先2後1

  1. Promise的then屬於microtasks

microtask和macrotask是Event Loop的知識點,關於Event Loop能夠參考阮一峯老師的《JavaScript 運行機制詳解:再談Event Loop》
此處咱們使用setTimeout來模擬then的microtasks(注:)

function resolve(value) {
  setTimeout(() => {
    if (that.state === PENDING) {
      that.state = RESOLVED
      that.value = value
      that.resolvedCallbacks.map(cb => cb(that.value))
    }
  }, 0)
}
function reject(value) {
  setTimeout(() => {
    if (that.state === PENDING) {
      that.state = REJECTED
      that.value = value
      that.rejectedCallbacks.map(cb => cb(that.value))
    }
  }, 0)
}
複製代碼
  1. resolve支持傳入Promise對象

咱們執行以下代碼:

let p = new Promise((resolve) => {
  var a = new Promise((resolve) => { resolve(1) });
  resolve(a);
});

p.then((res) => {
  console.log(res);
});
複製代碼

此處resolve傳入的是Promise對象,打印結果爲:

image.png

因此在resolve函數中須要對value作一次判斷

function resolve(value) {
  if (val instanceof Promise) {
    return val.then(resolve, reject);
  }
  setTimeout(() => {
    if (that.state === PENDING) {
      that.state = RESOLVED
      that.value = value
      that.resolvedCallbacks.map(cb => cb(that.value))
    }
  }, 0)
}
複製代碼
  1. then能夠鏈式調用

在Promise中,在then中執行return語句,返回的必定是Promise對象,這也是then可以鏈式調用的緣由。
首先咱們將then中的以下片斷

if (that.status === pending) {
    that.resolvedCallbacks.push(onFulfilled)
    that.rejectedCallbacks.push(onRejected)
  }
複製代碼

變形

if (that.status === pending) {
  that.resolvedCallbacks.push(()=>{onFulfilled(that.value)});
  that.rejectedCallbacks.push(()=>{onRejected(that.value)});
}
複製代碼

它們之間只是寫法的差別,效果相同。

由於咱們須要對then裏傳入的函數onFulfilled, onRejected返回的值進行判斷,因此咱們須要對then繼續改寫

if (that.status === pending) {
  that.resolvedCallbacks.push(()=>{const x = onFulfilled(that.value)});
  that.rejectedCallbacks.push(()=>{const x = onRejected(that.value)});
}
複製代碼

由於then返回的是Promise,因此繼續完善

if (that.status === pending) {
  return new Promise(resolve,reject){
  	that.resolvedCallbacks.push(()=>{const x = onFulfilled(that.value)});
  	that.rejectedCallbacks.push(()=>{const x = onRejected(that.error)});
  }
}
複製代碼

執行onFulfilled和onRejected時,使用try...catch...,因此繼續完善

let promise2 = null;
if (that.status === pending) {
  return promise2 = new Promise((resolve,reject)=>{
    that.resolvedCallbacks.push(()=>{
      try{
        const x = onFulfilled(that.value);
      }catch(e){
        reject(e);
      }
    });
    
    that.rejectedCallbacks.push(()=>{
      try{
        const x = onRejected(that.error);
      }catch(e){
        reject(e);
      }
    });
  });
}
複製代碼

上述x是onFulfilled(that.value)和onRejected(that.error)的返回值,爲了保證then能夠鏈式調用,也就是promise2的resolve可以resolve一個Promise對象,可是x返回的多是Promise對象,多是值,也多是函數,那麼此處須要對x進行適配一下。此時引入resolvePromise函數,實現以下:

/** * 對resolve 進行改造加強 針對x不一樣值狀況 進行處理 * @param {promise} promise2 promise1.then方法返回的新的promise對象 * @param {[type]} x promise1中onFulfilled的返回值 * @param {[type]} resolve promise2的resolve方法 * @param {[type]} reject promise2的reject方法 */
function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {  
        // 若是從onFulfilled中返回的x 就是promise2 就會致使循環引用報錯
        return reject(new TypeError('循環引用'));
    }

    let called = false; // 避免屢次調用
    // 若是x是一個promise對象 (該判斷和下面 判斷是否是thenable對象重複 因此無關緊要)
    if (x instanceof Promise) { // 得到它的終值 繼續resolve
        if (x.status === PENDING) { // 若是爲等待態需等待直至 x 被執行或拒絕 並解析y值
            x.then(y => {
                resolvePromise(promise2, y, resolve, reject);
            }, reason => {
                reject(reason);
            });
        } else { 
            // 若是 x 已經處於執行態/拒絕態(值已經被解析爲普通值),用相同的值執行傳遞下去 
            x.then(resolve, reject);
        }
        // 若是 x 爲對象或者函數
    } else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) {
        try { // 是不是thenable對象(具備then方法的對象/函數)
            let then = x.then;
            if (typeof then === 'function') {
                then.call(x, y => {
                    if(called) return;
                    called = true;
                    resolvePromise(promise2, y, resolve, reject);
                }, reason => {
                    if(called) return;
                    called = true;
                    reject(reason);
                })
            } else { // 說明是一個普通對象/函數
                resolve(x);
            }
        } catch(e) {
            if(called) return;
            called = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
}

複製代碼

此時that.status === pending的代碼塊也要繼續修改

if (that.status === pending) {
        return promise = new Promise1((resolve, reject) => {
            that.resolvedCallbacks.push(() => {
                try {
                    const x = onFulfilled(that.value);
                    resolvePromise(promise,x,resolve,reject);
                    } catch (e) {
                        reject(e);
                    }
                });
                
                that.rejectedCallbacks.push(() => {
                    try {
                        const x = onRejected(that.error);
                    } catch (e) {
                        reject(e);
                    }
                });
            })
    }	
複製代碼

同理that.status===fulfilled和that.status===rejecting的時候代碼以下:

if (that.status === FULFILLED) { // 成功態
        return promise2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                try{
                    let x = onFulfilled(that.value);
                    resolvePromise(promise2, x, resolve, reject); 
                } catch(e) {
                    reject(e); 
                }
            });
        })
    }

    if (that.status === REJECTED) {
        return promise2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                try {
                    let x = onRejected(that.reason);
                    resolvePromise(promise2, x, resolve, reject);
                } catch(e) {
                    reject(e);
                }
            });
        });
    }

複製代碼
  1. Promise的all和race

Promise.all的用法以下

const p = Promise.all([p1, p2, p3]).then((resolve)=>{},(reject)=>{});
複製代碼

Promise.all方法接受一個數組做爲參數,只有當數組的全部Promise對象的狀態所有fulfilled,纔會執行後續的then方法。
根據all的用法和特色,咱們推測Promise.all返回一個Promise對象,在Promise對象中去等待Promise數組對象的函數執行完畢,數組中的每一個對象執行完畢都+1,當等於數組的長度時,resolve數組對象中全部resolve出來的值。

Promise.all = function(promises) {
    return new Promise((resolve, reject) => {
        let done = gen(promises.length, resolve);
        promises.forEach((promise, index) => {
            promise.then((value) => {
                done(index, value)
                //每執行一次都會往values數組中放入promise對象數組成員resolve的值
                //當values的長度等於promise對象數組的長度時,resolve這個數組values
            }, reject)
        })
    })
}

//使用閉包,count和values在函數的聲明週期中都存在
function gen(length, resolve) {
    let count = 0;
    let values = [];
    return function(i, value) {
        values[i] = value;
        if (++count === length) {
            console.log(values);
            resolve(values);
        }
    }
}
複製代碼

Promise.race的用法和all相似,區別就是promise對象數組其中有一個fulfilled,就執行then方法,實現以下

Promise.race = function(promises) {
    return new Promise((resolve, reject) => {
        promises.forEach((promise, index) => {
           promise.then(resolve, reject);
        });
    });
}
複製代碼

須要注意的是all和race函數中的數組成員不必定是Promise對象,若是不是Promise提供了resolve方法將其轉化成Promise對象。resolve的實現很簡單以下:

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

至此一個比較規範的Promise實現了。

參考

《ES6入門-阮一峯》
《ES6 系列之咱們來聊聊 Promise》
《異步解決方案----Promise與Await》
《Promise原理講解 && 實現一個Promise對象》
《面試精選之Promise》
《Promise/A+》
《Promise之你看得懂的Promise》
《八段代碼完全掌握 Promise》
《Promise不會??看這裏!!!史上最通俗易懂的Promise!!!》
《Promise 必知必會(十道題)》

相關文章
相關標籤/搜索