手寫Promise,手把手過程

  網上不少手寫Promise。根據本身的理解,也寫了一份,曬出來但願能被你們指正。也給本身一個梳理的過程。初學者要辯證的看這個文檔,你須要對原生Promise很熟悉。html

  代碼其實很是簡單,不到100行代碼,主要是充分了解原生Promise都能作什麼,有什麼特徵。在根據這些功能特徵列出模擬Promise的需求,問題就解決了一大半了。這裏會一步步的列出原生Promise的功能特徵。再一步步的添加代碼。每一步代碼都是在上一步的代碼基礎上添加或修改得來的,在代碼中會標識出哪裏作了修改和增長,這樣就不用翻來翻去看上一步的代碼了,保證思路連貫性面試

  說明一下,這裏研究原生的Promise只看表象,不深刻分析。文中代碼的運行和測試都是在chrome【版本 81.0.4044.129(正式版本) (64 位)】中進行chrome

  文中代碼雖然已經都測試過了,可是不能保證在複製粘貼過程當中沒錯。因此最好理解以後本身敲一遍promise

還有更重要的是:這裏用setTimeout來模擬微任務,會和原生的Promise在程序中執行順序有所不一樣。這點必定要了解。 關於事件循環寫了 一個簡述文章,有興趣的能夠看一下:簡述JavaScript事件循環EventLoopbash

正文看起來有點長,其實都是重複的代碼佔的位置,內容很簡單異步

1、Promise是個容器

首先是模擬Promise須要實現的最基本的需求函數

  • promise這個容器,用來存儲異步或同步執行的結果。oop

  • 還要有狀態,來反映同步或者異步處理的階段。有三種狀態post

    1. pending 異步或同步的執行階段,這個時候結果值爲undefined
    2. fulfilled,一般表示同步或者異步成功返回告終果的狀態,這時候結果值記錄返回的正確結果
    3. rejected,一般表示同步或者異步出現錯誤時的狀態,這個時候結果值記錄錯誤的緣由
  • 在Promise對象外部不能直接訪問到Promise的狀態和值測試

  • 還須要有個執行同步或異步的函數(執行函數),以Promise參數的形式,在Promise構造函數中執行

上代碼:

/*'↓↓↓定義了MyPromise函數,參數executor(執行函數),數據類型是Funciotn,用來執行同步或異步操做↓↓↓'*/
/*'↓↓↓之因此用函數不用class,是爲了方便定義對象私有變量和私有方法↓↓↓'*/
function MyPromise(executor){
    /*'↓↓↓定義了三個狀態PENDING、FULFILLED、REJECTED↓↓↓'*/
    const PENDING = "pending";
    const FULFILLED = "fulfilled";
    const REJECTED = "rejected";
    
    /*'↓↓↓value變量存儲異步結果(Promise的值),pending狀態下是undefined,fulfilled狀態下是執行結果,rejected狀態下是錯誤緣由↓↓↓'*/
    let value;
    
    /*'↓↓↓state變量用來存儲Promise狀態,初始狀態是pending↓↓↓'*/
    let state = PENDING;
    
    /*'↓↓↓都定義好了,再運行執行函數↓↓↓'*/
    executor()
}

複製代碼

最基本的部分完成

2、內部操做狀態和值

上面定義了容器中的狀態和須要存儲值的變量,並且運行了用戶本身定義的執行方法,可是當執行函數有告終果,怎麼改變容器的狀態和存儲結果呢?這就須要定義操做狀態和值方法來處理。需求以下

  • 值和狀態只能改變一次
  • 操做狀態和值的方法不向外暴露,內部經過resolve方法把狀態改爲fulfilled,經過reject方法把狀態改變成rejected,並改變狀態對應的值。這兩個方法做爲參數傳給執行函數executor,由用戶決定何時經過這兩個方法改變狀態和值。

繼續完善MyPromise ↓↓↓↓↓

function MyPromise(executor){
    const PENDING   = "pending";
    const FULFILLED = "fulfilled";
    const REJECTED  = "rejected";
    let value;
    let state = PENDING;
    
    /*'↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓這裏是新加的改變狀態和值的方法↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓'*/
    function change(newState, newValue){ //'用來修改狀態和相應的值的方法'
        if (state === PENDING) {//'限制了只能在狀態pending下改變,這樣就保證狀態和值只能改一次'
        value = newState;
        state = newValue;
      }
    }
    
    let resolve = change.bind(this, FULFILLED);// '定義了resolve函數,只能把狀態改爲fulfilled'
    let reject = change.bind(this, REJECTED);// '定義了reject函數,只能把狀態改爲fulfilled'
    //'resolve和reject都是change的偏函數,其實綁定this沒啥意義,就是寫着方便,修改方便,只關注change就好了,也便於閱讀代碼'
    /*'↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑這裏是新加的改變狀態和值的方法↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑'*/
    
    executor(resolve, reject)// ←←←'經過executor參數,傳遞到Promise外部使用'
}
複製代碼

3、須要註冊回調函數

能存儲狀態和值了,也能修改狀態和值了,可是還須要在相應狀態下處理值的方法的呢,因此就須要註冊回調函數了。

Promise對象中有兩個方法then和catch註冊回調函數

  • then有兩個參數,第一個參數用來註冊狀態變爲fulfilled時候處理返回值的回調,而且當狀態改變以後,隨時註冊均可以處理那個已經固定了的返回值,第二個參數用來註冊狀態變爲rejected時候處理錯誤緣由的回調,而且當狀態改變以後,隨時註冊均可以處理那個已經固定了的錯誤緣由
  • catch方法註冊的回調等同於then的第二個函數的做用同樣,其實catch就是then的語法糖,至關於then(undefined,function)
  • catch和then都返回一個新的Promise
  • 一個Promise對象能夠註冊不少個回調,也就是new了一個Promise能夠分別點上不少then或者catch,這裏說的不是鏈式調用呦,是分別分別分別調用then或者catch。仍是舉個例子吧
let p = new Promise((resolve, reject) => {})
p.then(value => {})
p.then(value => {})
p.catch(value => {})
/*'↑↑↑↑↑↑上面的代碼說的是分別註冊不少回調↑↑↑↑↑↑↑'*/
/*'各個回調是獨立的,返回的新Promise也不是同一個,這至關於Promise狀態傳遞出現了分支,這個後面再展開'*/

/*'↓↓↓↓↓↓下面是鏈式調用,這裏說的不是這種狀況↓↓↓↓↓↓↓↓↓↓↓↓↓'*/
p.then(value => {}).then(value => {}).then(value => {}).then(value => {})

複製代碼

根據上面的需求先吧這兩個用來註冊回調的方法加上

function MyPromise(executor){
    const PENDING   = "pending";
    const FULFILLED = "fulfilled";
    const REJECTED  = "rejected";
    let value;
    let state = PENDING;
    
    function change(newState, newValue){
        if (state === PENDING) {
        value = newState;
        state = newValue;
      }
    }
    
    let resolve = change.bind(this, FULFILLED);
    let reject = change.bind(this, REJECTED);
    
    /*'↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓這裏是註冊回調的方法↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓'*/
    var onQueue = []; //'→→→→→爲了能註冊多個回調,定義了onQueue,存儲註冊的回調,等待狀態改變後調用'
    function register(onFulfilled, onRejected) {//'→→→→→用來註冊回調的方法'
        let nextPromise = new MyPromise((nextResolve, nextReject) => {
          /*'↓↓↓↓↓↓↓↓↓向onQueue中添加註冊的方法,用這種對象結構保存是爲了以後調用方便↓↓↓↓↓↓↓↓↓'*/
          /*'↓↓↓↓↓↓↓↓↓至於爲何在Promise裏寫,後面會的內容會詳細提到↓↓↓↓↓↓↓↓↓'*/
          onQueue.push({
            [FULFILLED]: { on: onFulfilled},
            [REJECTED]: { on: onRejected},
          });
        })
        return nextPromise;
    }
    this.then = register.bind(this);            //'→→→→→定義了Promise對象的then方法,註冊處理返回值的回調方法'
    this.catch = register.bind(this, undefined);//'→→→→→定義了Promise對象的catch方法,是then的語法糖'
    /*'↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑這裏是註冊回調的方法↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑'*/
    
    executor(resolve, reject)
}
複製代碼

4、須要一個機制,來處理註冊的回調。

當狀態不是pending的時候,就須要執行註冊的回調,那麼就須要有一個處理回調的機制。這裏添加一個run方法處理這個功能

在加功能以前仍是定需求,看看原生Promise都幹了些什麼

首先須要說明一下:這裏說的執行註冊的回調函數,並非說直接執行,要遵循js的event loop事件循環機制。原生的Promise是把回調放入微任務等待,等這次宏任務執行完畢,再執行當前微任務

這裏咱們用setTimeout這個宏任務來模擬微任務

再看看何時開始處理回調:

  • 當狀態從pending變爲fulfilled或rejected的時候,就須要處理註冊的回調
  • 當狀態不是pending的時候,註冊回調以後就須要立刻處理

不要着急,還有別的,看看鏈式調用都發生了什麼,上例子:

new Promise((resolve, reject) => {
    // resolve('p ok')
    reject('p err')
}).then(value => {
    console.log("成功1 "+value)
    return "p1"
}, error => {
    console.log("失敗1 "+error)//←←←←←←輸出這裏
    return "p1 err"
}).catch(err => {
    console.log("失敗2 "+error)
    return "p2 err"
}).then(value => {
    console.log("成功3 "+value)//←←←←←←輸出這裏
    return new Promise((resolve, reject) => {
        reject("新的Promise失敗了")
    })
}, error => {
    console.log("失敗3 "+error)
    return "p3 err"
}).then(value => {
    console.log("成功4 "+value)
}, error => {
    console.log("失敗4 "+error)//←←←←←←輸出這裏
})

//輸出:
//失敗1 p err
//test.html:56 成功3 p1 err
//test.html:66 失敗4 新的Promise失敗了


複製代碼

整了一個好長的鏈,簡單說明一下狀況,爲了說的清楚,用一個圖來解釋一下:

avatar
根據上面的例子總結出以下幾點

  • then方法返回的Promise狀態和值和用它註冊的回調函數返回值有關。
    1. 當回調函數返回值爲非Promise對象的時候,then返回的Promise對象的狀態是fulfilled,值爲回調函數的返回值
    2. 當回調函數返回值爲Promise對象的時候,then返回的Promise對象狀態和值,繼承回調函數返回的Promise對象的狀態和值。
  • 當前環節(當前then或者catch),沒有相應狀態的回調函數的時候,狀態和值會向下傳遞。

根據上述原生Promise特徵,繼續增長MyPromise功能,加一個run方法用來實現處理註冊的回調函數的機制

function MyPromise(executor){
    const PENDING   = "pending";
    const FULFILLED = "fulfilled";
    const REJECTED  = "rejected";
    let value;
    let state = PENDING;
    
    function change(newState, newValue){
        if (state === PENDING) {
        value = newState;
        state = newValue;
        run()//'→→→→→這裏執行run方法。在狀態改變的時候嘗試處理一下回調函數 } } let resolve = change.bind(this, FULFILLED); let reject = change.bind(this, REJECTED); /*'↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓這裏處理註冊的回調函數↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓'*/ function run(){ if (state === PENDING) { return; } while (onQueue.length) { let onObj = onQueue.shift();//'←←←從註冊的回調中拿出一份,放入下面模擬的微任務中' setTimeout(() => { //'←←←用setTimeout模擬微任務,把回調放入,等待執行' if (onObj[state].on) { //'←←←判斷當前狀態下,是否註冊了回調函數' let returnvalue = onObj[state].on(value);//'←←←有就運行回調函數,獲得返回值' if (returnvalue instanceof MyPromise) { //'←←←判斷返回值是否是MyPromise類型' /*'↓↓↓返回值是MyPromise類型,用這個MyPromise對象的then方法,能獲得返回MyPromise對象的狀態和值
                 ↓↓↓再利用nextPromise的resolve和reject方法做爲參數獲得狀態和值,這樣就實現了繼承狀態和值'*/ returnvalue.then(onObj[FULFILLED].next, onObj[REJECTED].next); } else { //'↓↓↓返回值不是MyPromise類型,直接改變nextPromise狀態爲fulfilled,值爲回調函數的返回值' onObj[FULFILLED].next(returnvalue); } } else { /*'↓↓↓當前狀態沒有註冊回調函數,
               ↓↓↓則利用保存的nextPromise對象的resolve或reject,改變nextPromise對象的狀態
               ↓↓↓這就至關於傳遞了狀態和值'*/ onObj[state].next(value); } }, 0); } } /*'↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑這裏處理註冊的回調函數↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑'*/ var onQueue = []; function register(onFulfilled, onRejected) { let nextPromise = new MyPromise((nextResolve, nextReject) => { onQueue.push({ /*"↓↓↓這裏添加了next屬性,爲何呢 ↓↓↓上面的需求中提到了,狀態和值會有向下傳遞的狀況,還會有繼承回調函數返回Promise對象狀態和值的狀況 ↓↓↓要想改變下一個Promise對象(nextPromise)狀態,只能經過它的resolve和reject方法 ↓↓↓因此這裏就把這倆方法提保存起來以便傳遞和繼承狀態使用↓↓↓↓"*/ [FULFILLED]: { on: onFulfilled, next: nextResolve}, [REJECTED]: { on: onRejected, next: nextReject},//'←←←之因此分開存,就是由於在方便向下傳遞狀態' }); }) run() //'→→→→→這裏執行run方法。用來處理註冊的回調函數,run方法裏有判斷,爲pending狀態不處理回調函數' return nextPromise; } this.then = register.bind(this); this.catch = register.bind(this, undefined); executor(resolve, reject) } 複製代碼

5、 報錯處理

仍是先上幾個例子,看看原生Promise特色,總結一下需求。

  • Promise錯誤是不向外拋出的

先來一個小實驗

new Promise(() => {
    xxx;// 用一個未定義的變量拋錯
})// 控制檯輸出了錯誤
console.log('我是後面的代碼')// '可是也輸出了這句,沒有被阻塞,說明沒有拋出到Promise外面'
複製代碼

上面的例子錯誤輸出了,後面的代碼也執行了。錯誤只是簡單的輸出,說明沒有拋出到Promise外面。 可是若是Promise的參數是否是function類型,會發生什麼

new Promise("hahaha") //Uncaught TypeError: Promise resolver hahaha is not a function
console.log('我是後面的代碼')//'這裏沒有輸出輸出了'
複製代碼

上面例子錯誤輸出了,後面的代碼沒有輸出。說明錯誤拋出來了,因此:

  • Promise參數不是function類型,會向外拋出錯誤

再來個例子,看看Promise是怎麼處理各個部分報錯的。

new Promise((resolve, reject) => {
    xxx;
    // resolve("ok")
    // reject('no')
    // xxx;
}).then(value => {                
    //xxx;
    console.log(value)//輸出位置0
},err => {                        
    //xxx;
    console.log("err1",err)//輸出位置1
}).catch(err => {                 
    console.log("err2",err)//輸出位置2
})

當Prosmise的執行函數有錯,輸出位置1輸出:err1 ReferenceError: xxx is not defined
當then第一個參數有錯誤,輸出位置2輸出:err1 ReferenceError: xxx is not defined
當then第二個參數有錯誤,輸出位置2輸出:err2 ReferenceError: xxx is not defined
這個例子把then的第二個參數去掉
當Prosmise的執行函數和then第一個參數有錯,都在輸出位置2輸出:err2 ReferenceError: xxx is not defined
複製代碼

你們能夠再變換一些方式測試錯誤處理的方式。根據以上例子能夠總結

  • Promise對象執行函數報錯,會把狀態改爲rejected,調用自身註冊的onrejected函數。
  • 若是在Promise對象狀態改變以後報錯,則不會獲得處理,更不會拋到對象外
  • 若是自身沒有註冊onrejected函數,錯誤逐級向下傳遞的。直到最後。也就是說,錯誤會在離它最近的onrejected函數獲得處理。 這與rejected狀態傳遞方式同樣。
function MyPromise(executor){
    const PENDING   = "pending";
    const FULFILLED = "fulfilled";
    const REJECTED  = "rejected";
    let value;
    let state = PENDING;
    
    function change(newState, newValue){
        if (state === PENDING) {
        value = newState;
        state = newValue;
        run()
      }
    }
    
    let resolve = change.bind(this, FULFILLED);
    let reject = change.bind(this, REJECTED);
    
    function run(){
      if (state === PENDING) {
        return;
      }
      while (onQueue.length) {
        let onObj = onQueue.shift();
        setTimeout(() => {
          if (onObj[state].on) {
            try{//'在加了回調函數運行時抓取錯誤'
              let returnvalue = onObj[state].on(value);
              if (returnvalue instanceof MyPromise) {
                returnvalue.then(onObj[FULFILLED].next, onObj[REJECTED].next);
              } else {
                onObj[FULFILLED].next(returnvalue);   
              }
            }catch(error){
              onObj[REJECTED].next(error);//'←←←若是回調函數報錯則,以rejected的狀態向下傳遞'
            }
          } else {
            onObj[state].next(value);
          }  
        }, 0);
      }
    }
    
    var onQueue = []; 
    function register(onFulfilled, onRejected) {
        let nextPromise = new MyPromise((nextResolve, nextReject) => {
          onQueue.push({
            [FULFILLED]: { on: onFulfilled, next: nextResolve},
            [REJECTED]: { on: onRejected, next: nextReject},
          });
        })
        run() 
        return nextPromise;
    }
    this.then = register.bind(this);
    this.catch = register.bind(this, undefined);
    
    //'↓↓↓加了判斷,若是executor不是函數類型,就向外拋錯'
    if (!(executor instanceof Function)) {
      throw new TypeError(executor + " 不是個函數。親!MyPromise參數得是個函數的呢");
    }
    
    //'↓↓↓這裏又加了try爲執行函數運行時抓取錯誤'
    //'↓↓↓可是有個問題就是若是executor不是函數,光加個try就不會向外拋出錯誤了,因此在這前邊再加個判斷'
    try {
      executor(resolve, reject);
    } catch (error) {
      /*'↓↓↓若是執行函數報錯,改變自身狀態爲rejected。 ↓↓↓若是在狀態改變以後報錯,也會執行這裏,可是前面已經限制了狀態只能改變一次。 ↓↓↓在這裏調用reject方法就沒有用了。達到了狀態改變以後報錯不處理的效果。'*/
      reject(error);
    }
}
複製代碼

寫到這裏promise最基本的功能就實現了。可是有那麼一點點特徵尚未,是錦上添花的功能,是啥呢。上例子

new Promise((resolve, reject) => {
    // xxx;
    resolve("ok")
    // reject('no')
})
//resolve時,控制檯沒輸出
//reject時,控制檯輸出:Uncaught (in promise) no。意思就是錯誤沒有處理
//在狀態改變以前的錯誤,報錯輸出在控制檯,可是也只是輸出而已不是拋出錯誤。由於不會阻塞代碼
複製代碼

上面例子說明報錯和rejected狀態沒有被處理,雖然不會拋出,也不會影響程序。可是會在控制檯有紅字提示。因此最後把這個提示功能加上,下面的代碼調整了一下順秩序,看着更順眼點,再添加提示的功能

function MyPromise(executor){
    const PENDING   = "pending";
    const FULFILLED = "fulfilled";
    const REJECTED  = "rejected";
    let value;
    let state = PENDING;
    
    let tipTask;//'爲了能移除提示任務用的'
    
    function change(newState, newValue){
        if (state === PENDING) {
        value = newState;
        state = newValue;
        if (!onQueue.length && state === REJECTED) {
          /* '當狀態爲rejected,並且沒有註冊回調函數,則在任務中放入一個提示任務。 若是在MyPromise運行的這個宏任務中註冊了回調,則在run中提示任務被移除 直到最後,確定會有沒有註冊回調函數的MyPromise對象。這個對象就會執行這個提示任務了 例如一個鏈式調用p.then().then()....then()不可能無窮的,總會有最後一個。 這最後一個返回的promise就不會被註冊回調。因此這裏添加的提示任務就會被執行了 ' */
          tipTask = setTimeout(() => {//'爲了能移除這個任務。把變量放在MyPromise函數做用域下'
            console.error("在MyPromise裏,須要註冊一個處理錯誤的回調 \n" + (value || ''));
          }, 0);
        }
        run()
      }
    }
    
    function run(){
      if (state === PENDING) {
        return;
      }
      while (onQueue.length) {
        clearTimeout(tipTask);//'若是註冊了回調。這裏把change方法里加的提示任務移除掉'
        
        let onObj = onQueue.shift();
        setTimeout(() => {
          if (onObj[state].on) {
            try{
              let returnvalue = onObj[state].on(value);
              if (returnvalue instanceof MyPromise) {
                returnvalue.then(onObj[FULFILLED].next, onObj[REJECTED].next);
              } else {
                onObj[FULFILLED].next(returnvalue);   
              }
            }catch(error){
              onObj[REJECTED].next(error);
            }
          } else {
            onObj[state].next(value);
          }  
        }, 0);
      }
    }
    
    var onQueue = []; 
    function register(onFulfilled, onRejected) {
        let nextPromise = new MyPromise((nextResolve, nextReject) => {
          onQueue.push({
            [FULFILLED]: { on: onFulfilled, next: nextResolve},
            [REJECTED]: { on: onRejected, next: nextReject},
          });
        })
        run() 
        return nextPromise;
    }
    
    let resolve = change.bind(this, FULFILLED);
    let reject = change.bind(this, REJECTED);
    
    this.then = register.bind(this);
    this.catch = register.bind(this, undefined);
    
    if (!(executor instanceof Function)) {
      throw new TypeError(executor + " 不是個函數。親!MyPromise參數得是個函數的呢");
    }
    
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
}
複製代碼

好了這就獲得一個相對完美的模擬Promise的手寫代碼了。

其實Uncaught (in promise) no或者內部報錯沒處理在控制檯輸出,實際上在chrome中是在全部當前存在的宏任務隊列任務(不是全部,是當前,也就是在運行這個Promise的宏任務運行時,所存在的宏任務)都執行完畢以後再輸出(我的懷疑這個提示用的是否是setTimeout0呀哈哈哈)。

這裏用setTimeout模擬的微任務,在js事件循環(event loop)循序上會和原生Promise有區別。

本文但願能給你們幫助,也但願能獲得指點。文中使用的詞語都是相對口語化的,若是您在正式場合使用,好比面試中,請使用更高大上的專業用語。謝謝

相關文章
相關標籤/搜索