摯愛原生之Promise深刻淺出

前言

做爲程序猿,能夠沒有女友(好像也只能接收找不到的現實),但絕對不能沒有格子衫、護髮素····和對原生的摯愛,今天,我們就來手寫一個Promise吧
看到掘金裏有一篇文章,45道Promise面試,讀完以爲很好,也正是由於寫完這些題目以爲本身深深的不足,才促成了本文的出現,表示衷心感謝;你們也能夠去試試看,本身到底掌握程度如何(看完本文,代碼層面解決此文章的全部題目,反正本菜已經解決了,皮~)java

目標

實現一個符合 Promise A+ 規範的我的版promise(經過promises-aplus-tests測試)node

  • 特色
    • 按部就班實現四個版本,而且每一個版本都有測試用例
    • 我的整理思惟導圖,清晰明瞭

問題驅動

代碼題

  • 1.1 牛客網
const promise = new Promise((resolve, reject) => {
    resolve('success1');
    reject('error');
    resolve('success2');
});

promise.then((res) => {
    console.log('then:', res);
}).catch((err) => {
    console.log('catch:', err);
})
複製代碼
  • 1.2 騰訊
Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)
複製代碼

簡答題

  • 談談對promise的理解
  • Promise的問題?處理辦法?
  • 方法實現
    • Promise.resolve(value)
    • .catch
    • .all
    • .race

開始

Promise A+是什麼

不照搬照套定義,簡言之,由於Promise已經是已經是業內通用,因此必然要保證在使用你實現的promise的過程當中行爲的可靠性,其實也就是一個【面向接口編程】的感受吧,標準化才穩定,官話建議點進連接看看,小菜整理後面試

  1. 在new Promise是須要傳遞一個執行器函數,executor 這個函數默認就會被執行 當即執行
  2. 每一個promise 都有三個狀態 pending 等待態 fulfilled 成功態 rejected 失敗態
  3. 默認建立一個promise 是等待態 默認提供給你兩個函數 resolve讓promise變成成功態,reject讓promise變成失敗態
  4. 每一個promise的實例都具有一個then方法 then方法中傳遞兩個參數1.成功的回調 2.失敗的回調
  5. 如何讓promise變成失敗態 reject() / 能夠拋出一個錯誤
  6. 若是屢次調用成功或者失敗 只會執行第一次, 一旦狀態變化了 就不能在變成成功或者失敗了 附送整理思惟導圖
    是否符合標準,則須要經過promises-aplus-tests進行校驗,文末會詳細講解

初版實現

  1. 第一點 new Promise傳入的回調函數,也就是圖中的executor是會同步執行的
  2. 第二點 每一個promise都有三個狀態,默認是PENDING(等待態)
  3. 第三點 爲executor傳兩個回調,用於改變狀態
  4. 第五點 狀態的控制
  • 首先 定義三個狀態常量便於控制
const PENDING = 'PENDING' // 等待態 
const FULFILLED = 'FULFILLED' // 成功態
const REJECTED = 'REJECTED' /
複製代碼
  • 定義回調,傳入
// 只有狀態是等待態的時候 才能夠更新狀態
    let resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED
        this.value = value
      }
    }
    let reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED
        this.reason = reason
      }
    }
複製代碼
  • 第4點,定義then方法,參數是用戶傳過來的 成功的回調onFulfilled 和 失敗的回調onRejected
then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value)
    }
    if (this.status === REJECTED) {
      onRejected(this.reason)
    }
  }
複製代碼
  • 彙總
const PENDING = 'PENDING' // 等待態 
const FULFILLED = 'FULFILLED' // 成功態
const REJECTED = 'REJECTED' // 

class Promise {
  constructor(executor) {
    this.status = PENDING; // 默認是等待態
    this.value = undefined; // 用於記錄成功的數據
    this.reason = undefined; // 用於記錄失敗的緣由
    // 只有狀態是等待態的時候 才能夠更新狀態
    let resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED
        this.value = value
      }
    }
    let reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED
        this.reason = reason
      }
    }
    // executor 執行的時候 須要傳入兩個參數,給用戶來改變狀態的
    try {
      executor(resolve, reject);
    } catch (e) { // 表示當前有異常,那就使用這個異常做爲promise失敗的緣由
      reject(e)
    }
  }
  then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value)
    }
    if (this.status === REJECTED) {
      onRejected(this.reason)
    }
  }
}
module.exports = Promise
複製代碼

測試用例

會輸出 fail reason 重點想下爲何不是fail 我失敗了 要理解非pengding狀態下不會改變任何東西ajax

let Promise = require('./promise')
let promise = new Promise((resolve,reject)=>{

     reject('reason');
     resolve('value')
     throw new Error('我失敗了');
})
promise.then((success)=>{
    console.log('success',success)
},(err)=>{
    console.log('fail',err)
});

複製代碼

遺留問題

當executor執行異步邏輯時,會存在then方法調用時狀態仍是pengding的狀況,由於異步的回調會入異步隊列中,後於同步代碼執行。spring

xmind總結

第二版實現

  1. 異步問題基於訂閱發佈設計模式進行解決,then方法中pengding狀態進行處理,將事件分別壓入對應事件存儲隊列(訂閱)
  2. 狀態改變函數(resolve,reject)中遍歷調用對應事件存儲隊列(發佈)
  3. 注意這裏用了一個箭頭函數包裹,實際上是一個AOP面向切片的思想(spring兩大支柱,懷念java哈哈),這樣咱們就能夠在調用用戶傳來的函數先後執行本身的邏輯,建議好好體會
  • 定義回調隊列
this.onResolvedCallbacks = []; // 存放成功時的回調
        this.onRejectedCallbacks = []; // 存放失敗時的回調
複製代碼
  • then中新增pengding狀態處理
if(this.status === PENDING){ // 發佈訂閱
            this.onResolvedCallbacks.push(()=>{
                // TODO ...
                onFulfilled(this.value);
            });
            this.onRejectedCallbacks.push(()=>{
                onRejected(this.reason)
            })
        }
複製代碼
  • 改寫狀態改變函數,新增清空隊列操做
// 只有狀態是等待態的時候 才能夠更新狀態
        let resolve = (value) => {
            if (this.status === PENDING) {
                this.status = FULFILLED
                this.value = value
                this.onResolvedCallbacks.forEach(fn=>fn()); // 發佈的過程
            }
        }
        let reject = (reason) => {
            if (this.status === PENDING) {
                this.status = REJECTED
                this.reason = reason
                this.onRejectedCallbacks.forEach(fn=>fn());
            }
        }
複製代碼

測試用例

let Promise = require('./promise')
let promise = new Promise((resolve,reject)=>{
    setTimeout(() => { // 異步的
        resolve('value') //此時若是調用了resolve 就讓剛纔存儲的成功的回調函數去執行
    }, 1000);
  
})
// 同一個promise實例 能夠then屢次
// 核心就是發佈訂閱模式
promise.then((success)=>{ // 若是調用then的時候沒有成功也沒有失敗。我能夠先保存成功和失敗的回調
    console.log('success',success)
},(err)=>{
    console.log('fail',err)
});
promise.then((success)=>{
    console.log('success',success)
},(err)=>{
    console.log('fail',err)
});
複製代碼

xmind總結

第三版(最終版)

  1. 實現鏈式調用:then返回一個promise
  2. 重點在於處理用戶調用then時傳過來的onFulfilled, onRejected的返回值x
    1. x 是一個普通值 就會讓下一個promise變成成功態
    2. x 有多是一個promise,我須要採用這個promise的狀態
  3. 實現對別人實現的Promise的兼容性處理
  4. 值穿透特性實現:若是用戶調用then時傳過來的處理函數不是函數而是一個值類型,則向下傳遞

實現鏈式調用

挺簡單的,該寫下then方法,new一個新的promise返回就能夠了npm

then(onFulfilled, onRejected) {
        // 可選參數的處理
        onFulfilled = typeof onFulfilled === 'function'?onFulfilled:val=>val;  
        onRejected = typeof onRejected === 'function'?onRejected:err=>{throw err}
        // 遞歸
        let promise2 = new Promise((resolve, reject) => {
            # 原有邏輯
        })
        return promise2;
    }
複製代碼

處理用戶調用then的返回值x及兼容Promise

這是關鍵,特色再加個xmind 編程

  1. 獲取到用戶的返回值,重點看思路:
    1. 由於用戶可能會返回一個promise1,而這個promise又有可能返回一個promise,簡言之,無限有規律循環,則確定是定義最小單元函數
    2. 最小單元函數遞歸思路:終止條件+最小單元
    3. 最小單元無疑是用戶返回一個promise,終止條件是:用戶返回一個普通值;
    4. 參數定義的思路:
      1. 由於須要判斷是否是返回了同一個promise實例(若是和promise2 是同一我的 x 永遠不能成功或者失敗,因此就卡死了,咱們須要直接報錯便可),因此參數須要傳遞then要返回的promise
      2. 用戶的返回值、成功、失敗回調確定要傳
  2. 細節處理
    1. 要作容錯處理,即在遞歸的方法外面加一層trycatch
    2. 要考慮用戶「發佈」時,若是訂閱了不少事件,這些事件之間的順序須要經過異步隊列進行保證(即一個promise實例屢次then,promise.then();promise.then();注意,這個鏈式調用不是一個概念,鏈式調用是promise.then().then())
then改寫
then(onFulfilled, onRejected) {
        // 可選參數的處理
        onFulfilled = typeof onFulfilled === 'function'?onFulfilled:val=>val;  
        onRejected = typeof onRejected === 'function'?onRejected:err=>{throw err}
        // 遞歸
        let promise2 = new Promise((resolve, reject) => {
            if (this.status === FULFILLED) {
                setTimeout(() => {
                    try {
                        let x = onFulfilled(this.value);
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            }
            if (this.status === REJECTED) {
                setTimeout(() => {
                    try {
                        let x = onRejected(this.reason);
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            }
            if (this.status === PENDING) {
                this.onResolvedCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            let x = onFulfilled(this.value);
                            resolvePromise(promise2, x, resolve, reject)
                        } catch (e) {
                            reject(e);
                        }
                    }, 0)
                });
                this.onRejectedCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            let x = onRejected(this.reason);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    }, 0)
                })
            }
        })
        return promise2;
    }
複製代碼
resolvePromise實現
const resolvePromise = (promise2, x, resolve, reject) => {
    // 判斷 可能你的promise要和別人的promise來混用
    // 可能不一樣的promise庫之間要相互調用
    if (promise2 === x) { // x 若是和promise2 是同一我的 x 永遠不能成功或者失敗,因此就卡死了,咱們須要直接報錯便可
        return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
    }
    // ------ 咱們要判斷x的狀態  判斷x 是否是promise-----
    // 1.先判斷他是否是對象或者函數
    if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
        // x 須要是一個對象或者是函數
        let called; // 爲了考慮別人promise 不健壯因此咱們須要本身去判斷一下,若是調用失敗不能成功,調用成功不能失敗,不能屢次調用成功或者失敗
        try{
            let then = x.then; // 取出then方法 這個then方法是採用defineProperty來定義的
            if(typeof then === 'function'){
                // 判斷then是否是一個函數,若是then 不是一個函數 說明不是promise 
                // 只能認準他是一個promise了 
                then.call(x, y =>{ // 若是x是一個promise 就採用這個promise的返回結果
                    if(called) return;
                    called = true
                    resolvePromise(promise2, y, resolve, reject); // 繼續解析成功的值
                },r=>{
                    if(called) return;
                    called = true
                    reject(r); // 直接用r 做爲失敗的結果
                })
            }else{
                // x={then:'123'}
                resolve(x);
            }
        }catch(e){
            if(called) return;
            called = true
            reject(e); // 去then失敗了 直接觸發promise2的失敗邏輯
        }
    } else {
        // 確定不是promise
        resolve(x); // 直接成功便可
    }
}
複製代碼
綜上總結
console.log('-------------- my ---------------')
// 宏
const PENDING = 'PENDING' // 等待態 
const FULFILLED = 'FULFILLED' // 成功態
const REJECTED = 'REJECTED' // 

const resolvePromise = (promise2, x, resolve, reject) => {
    // 判斷 可能你的promise要和別人的promise來混用
    // 可能不一樣的promise庫之間要相互調用
    if (promise2 === x) { // x 若是和promise2 是同一我的 x 永遠不能成功或者失敗,因此就卡死了,咱們須要直接報錯便可
        return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
    }
    // ------ 咱們要判斷x的狀態  判斷x 是否是promise-----
    // 1.先判斷他是否是對象或者函數
    if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
        // x 須要是一個對象或者是函數
        let called; // 爲了考慮別人promise 不健壯因此咱們須要本身去判斷一下,若是調用失敗不能成功,調用成功不能失敗,不能屢次調用成功或者失敗
        try{
            let then = x.then; // 取出then方法 這個then方法是採用defineProperty來定義的
            if(typeof then === 'function'){
                // 判斷then是否是一個函數,若是then 不是一個函數 說明不是promise 
                // 只能認準他是一個promise了 
                then.call(x, y =>{ // 若是x是一個promise 就採用這個promise的返回結果
                    if(called) return;
                    called = true
                    resolvePromise(promise2, y, resolve, reject); // 繼續解析成功的值
                },r=>{
                    if(called) return;
                    called = true
                    reject(r); // 直接用r 做爲失敗的結果
                })
            }else{
                // x={then:'123'}
                resolve(x);
            }
        }catch(e){
            if(called) return;
            called = true
            reject(e); // 去then失敗了 直接觸發promise2的失敗邏輯
        }
    } else {
        // 確定不是promise
        resolve(x); // 直接成功便可
    }
}
class Promise {
    constructor(executor) {
        this.status = PENDING; // 默認是等待態
        this.value = undefined;
        this.reason = undefined;
        this.onResolvedCallbacks = []; // 存放成功時的回調
        this.onRejectedCallbacks = []; // 存放失敗時的回調
        let resolve = (value) => {
            if (this.status === PENDING) {
                this.status = FULFILLED
                this.value = value
                this.onResolvedCallbacks.forEach(fn => fn());
            }
        }
        let reject = (reason) => {
            if (this.status === PENDING) {
                this.status = REJECTED
                this.reason = reason
                this.onRejectedCallbacks.forEach(fn => fn());
            }
        }
        try { // try + catch 只能捕獲同步異常
            executor(resolve, reject);
        } catch (e) {
            console.log(e);
            reject(e)
        }
    }
    // 只要x 是一個普通值 就會讓下一個promise變成成功態
    // 這個x 有多是一個promise,我須要採用這個promise的狀態
    then(onFulfilled, onRejected) {
        // 可選參數的處理
        onFulfilled = typeof onFulfilled === 'function'?onFulfilled:val=>val;  
        onRejected = typeof onRejected === 'function'?onRejected:err=>{throw err}
        // 遞歸
        let promise2 = new Promise((resolve, reject) => {
            if (this.status === FULFILLED) {
                setTimeout(() => {
                    try {
                        let x = onFulfilled(this.value);
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            }
            if (this.status === REJECTED) {
                setTimeout(() => {
                    try {
                        let x = onRejected(this.reason);
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            }
            if (this.status === PENDING) {
                this.onResolvedCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            let x = onFulfilled(this.value);
                            resolvePromise(promise2, x, resolve, reject)
                        } catch (e) {
                            reject(e);
                        }
                    }, 0)
                });
                this.onRejectedCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            let x = onRejected(this.reason);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    }, 0)
                })
            }
        })

        return promise2;
    }
}

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

測試 promises-aplus-tests

promisesaplus.com/(即Promise A+) 是一個介紹promise如何實現的一個網站,並提供了一個promises-aplus-tests的npm全局包用於測試,簡單三步走設計模式

  • 全局安裝:npm install promises-aplus-tests -g
  • 在本身實現的Promise上掛載一個靜態屬性deferred,值是一個函數,返回一個有promise屬性的對象,promise屬性值是一個對象;(有點暈?不用麻煩了,簡單來講就是把下面代碼加在本身實現的promise文件裏就能夠了,上面那個最終版的也貼心的加好了)
Promise.deferred = function () {
    let dfd = {};
    dfd.promise = new Promise((resolve,reject)=>{
        dfd.resolve = resolve;
        dfd.reject = reject;
    })
    return dfd;
}
複製代碼
  • 執行測試命令promises-aplus-tests promise.js

如圖則成功

擴展 經常使用方法實現

直接上代碼啦,相信能堅持到如今的小夥伴確定比本南方小菜強,看代碼秒懂系列數組

靜態方法
  • all 此處廢話一句,由於面試超級常考 - 用法:接收promise數組,返回一個promise實例,而且其resolve中能夠拿到:一個包含全部請求的結果的數組且順序和傳過來的數組一一對應 - 思路:全部異步順序問題,都要想一想計時器,即定義一個方法,在其中進行判斷,若是知足條件才向下執行(很像函數科裏化) 1. 因返回能夠then,因此必然是返回一個promise實例 2. 遍歷數組,then拿到值 3. 定義一個「計時器」函數,用於記錄返回值個數,達到了傳進來的數組個數才調用用戶的then方法
Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    let results = []; let i = 0;
    function processData(index, data) {
      results[index] = data; // let arr = []  arr[2] = 100
      if (++i === promises.length) {
        resolve(results);
      }
    }
    for (let i = 0; i < promises.length; i++) {
      let p = promises[i];
      p.then((data) => { // 成功後把結果和當前索引 關聯起來
        processData(i, data);
      }, reject);
    }
  })
}
複製代碼
  • race
Promise.race = function (promises) {
  return new Promise((resolve, reject) => {
    for (let i = 0; i < promises.length; i++) {
      let p = promises[i];
      p.then(resolve, reject);
    }
  })
}
複製代碼
  • reject
Promise.reject = function (reason) {
  return new Promise((resolve, reject) => {
    reject(reason)
  })
}
複製代碼
  • resolve
Promise.resolve = function (value) {
  return new Promise((resolve, reject) => {
    resolve(value);
  })
}
複製代碼
實例方法
  • catch
Promise.prototype.catch = function (onrejected) {
  return this.then(null, onrejected)
}
複製代碼

回到原來問題

知道原理懼怕面試題嗎?來吧promise

  1. 初版的實現就已經解決問題了,promise只能狀態被改變一次,因此答案是:then success1
  2. 挺經典的題目,更顯得原理的重要性
    1. Promise.resolve(1) 至關於返回一個狀態爲成功的promise實例,詳見【擴展 經常使用方法實現】
new Promise((resolve, reject) => {
        resolve(1)
      }).then(2)
    .then(Promise.resolve(3))
    .then(console.log)
複製代碼
2. 此處resolve用戶傳了個普通值/對象,當傳非函數時,promise內部忽略傳遞值並傳一個默認函數(貼心的截個圖,val=>val的形式,不過仍是建議若是不熟悉去看看第三版)值傳遞
複製代碼

new Promise((resolve, reject) => {
        resolve(1)
      }).then(val=>val)
    .then(val=>val)
    .then(console.log)
複製代碼
3. 初版就能夠看到,在最開始的resolve(1)時,內部屬性this.val已是1了,後面均無改變val的操做,故最後一次傳來一個log函數,就打印出來了
4. 答案:1
複製代碼

附送 (若有侵權,請大佬直接聯繫我)

恭喜您堅持下來了,建議能夠本身跑一跑,畢竟測試用例都寫了,如今,再去試試本身的實力吧~~,再次表示感謝45道Promise面試

總結

不少時候,不只要知其然,還要知其因此然;儘管咱們在現在資源豐富、文檔普及的時代,學會一個工具的使用仍是很快的,成爲一個cv工程師很簡單,但也就很簡單的失去了擁有那種計算機行業的快樂的能力,【幹一行愛一行】纔是最關鍵的碼農精神所在;

我是【南方小菜】,最愛代碼世界;繼續努力,將來可期;

摯愛原生系列

摯愛原生之ajax

摯愛原生之node服務器

相關文章
相關標籤/搜索