從設計模式角度分析Promise:手撕Promise並不難


前言

Promise做爲異步編程的一種解決方案,比傳統的回調和事件更增強大,也是學習前端所必需要掌握的。做爲一個有追求的前端,不只要熟練掌握Promise的用法,並且要對其實現原理有必定的理解(說白了,就是面試裝逼必備)。雖然網上有不少Promise的實現代碼,幾百行的,但我的以爲,不對異步編程和Promise有必定的理解,那些代碼也就是一個板子而已(面試可不能敲板子)。首先默認讀者都對Promise對象比較熟悉了,而後將從前端最經常使用的設計模式:發佈-訂閱和觀察者模式的角度來一步一步的實現Promise。javascript

從異步編程提及

既然Promise是一種異步解決方案,那麼在沒有Promise對象以前是怎麼作異步處理的呢?有兩種方法:回調函數和發佈-訂閱或觀察者設計模式。(關於實現異步編程的更多方式請參考個人文章:JavaScript實現異步編程的5種方式前端

回調函數(callback function)

相信回調函數讀者都不陌生,畢竟最先接觸的也就是回調函數了,並且用回調函數作異步處理也很簡單,以nodejs文件系統模塊fs爲例,讀取一個文件通常都會這麼作java

fs.readFile("h.js", (err, data) => {
  console.log(data.toString())
});複製代碼

其缺點也很明顯,當異步流程變得複雜,那麼回調也會變得很複雜,有時也叫作」回調地獄」,就以文件複製爲例node

fs.exists("h.js", exists => { // 文件是否存在
  if (exists) {
    fs.readFile("h.js", (err, data) => { // 讀文件
      fs.mkdir(__dirname + "/js/", err => { // 建立目錄
        fs.writeFile(__dirname + "/js/h.js", data, err => { // 寫文件
          console.log("複製成功,再回調下去,代碼真的很難看得懂")
        })
      });
    });
  }
});複製代碼

其實代碼仍是能閱讀的,感謝JS設計者沒有把函數的花括號給去掉。像沒有花括號的python寫回調就是(就是個笑話。不是說python很差,畢竟JavaScript是世界上最好的語言)python

# 這代碼屬實無法看啊
def callback_1():
      # processing ...
  def callback_2():
      # processing.....
      def callback_3():
          # processing ....
          def callback_4():
              #processing .....
              def callback_5():
                  # processing ......
              async_function1(callback_5)
          async_function2(callback_4)
      async_function3(callback_3)
  async_function4(callback_2)
async_function5(callback_1)複製代碼

發佈-訂閱與觀察者設計模式

第一次學設計模式仍是在學Java和C++的時候,畢竟設計模式就是基於面向對象,讓對象解耦而提出的。發佈訂閱設計模式和觀察者模式很像,可是有點細微的區別(面試考點來了)面試

觀察者模式 在軟件設計中是一個對象,維護一個依賴列表,當任何狀態發生改變自動通知它們。
發佈-訂閱模式是一種消息傳遞模式,消息的 發佈者 (Publishers)通常將消息發佈到特定消息中心, 訂閱者( Subscriber) 能夠按照本身的需求從消息中心訂閱信息,跟消息隊列挺相似的

在觀察者模式只有兩種組件:接收者和發佈者,而發佈-訂閱模式中則有三種組件:發佈者、消息中心和接收者。編程


在代碼實現上的差別也比較明顯設計模式

觀察者設計模式promise

// 觀察者設計模式
class Observer {
  constructor () {
    this.observerList = [];
  }

  subscribe (observer) {
    this.observerList.push(observer)
  }

  notifyAll (value) {
    this.observerList.forEach(observe => observe(value))
  }
}複製代碼

發佈-訂閱設計模式(nodejs EventEmitter)閉包

// 發佈訂閱
class EventEmitter {
  constructor () {
    this.eventChannel = {}; // 消息中心
  }

  // subscribe
  on (event, callback) {
    this.eventChannel[event] ? this.eventChannel[event].push(callback) : this.eventChannel[event] = [callback]
  }

  // publish
  emit (event, ...args) {
    this.eventChannel[event] && this.eventChannel[event].forEach(callback => callback(...args))
  }

  // remove event
  remove (event) {
    if (this.eventChannel[event]) {
      delete this.eventChannel[event]
    }
  }

  // once event
  once (event, callback) {
    this.on(event, (...args) => {
      callback(...args);
      this.remove(event)
    })
  }
}複製代碼

從代碼中也能看出他們的區別,觀察者模式不對事件進行分類,當有事件時,將通知全部觀察者。發佈-訂閱設計模式對事件進行了分類,觸發不一樣的事件,將通知不一樣的觀察者。因此能夠認爲後者就是前者的一個升級版,對通知事件作了更細粒度的劃分。

發佈-訂閱和觀察者在異步中的應用

// 觀察者
const observer = new Observer();
observer.subscribe(value => {
  console.log("第一個觀察者,接收到的值爲:");
  console.log(value)
});
observer.subscribe(value => {
  console.log("第二個觀察者,接收到的值爲");
  console.log(value)
});
fs.readFile("h.js", (err, data) => {
  observer.notifyAll(data.toString())
});複製代碼

// 發佈-訂閱
const event = new EventEmitter();
event.on("err", console.log);
event.on("data", data => {
  // do something
  console.log(data)
});
fs.readFile("h.js", (err, data) => {
  if (err) event.emit("err", err);
  event.emit("data", data.toString())
});複製代碼

兩種設計模式在異步編程中,都是經過註冊全局觀察者或全局事件,而後在異步環境裏通知全部觀察者或觸發特定事件來實現異步編程。

劣勢也很明顯,好比全局觀察者/事件過多難以維護,事件名命衝突等等,所以Promise便誕生了。

從觀察者設計模式的角度分析和實現Promise

Promise在必定程度上繼承了觀察者和發佈-訂閱設計模式的思想,咱們先從一段Promise代碼開始,來分析Promise是如何使用觀察者設計模式

const asyncReadFile = filename => new Promise((resolve) => {
  fs.readFile(filename, (err, data) => {
    resolve(data.toString()); // 發佈者 至關於觀察者模式的notifyAll(value) 或者發佈訂閱模式的emit
  });
});

asyncReadFile("h.js").then(value => { // 訂閱者 至關於觀察者模式的subscribe(value => console.log(value)) 或者發佈訂閱模式的on
  console.log(value);
});複製代碼

從上面的Promise代碼中,我以爲Promise方案優於前面的發佈-訂閱/觀察者方案的緣由就是:對異步任務的封裝,事件發佈者在回調函數裏(resolve),事件接收者在對象方法裏(then()),使用局部事件,對二者進行了更好的封裝,而不是扔在全局中。

Promise實現

基於上面的思想,咱們能夠實現一個簡單的Promise:MyPromise

class MyPromise {
  constructor (run) { // run 函數 (resolve) => any
    this.observerList = [];
    const notifyAll = value => this.observerList.forEach(callback => callback(value));
    run(notifyAll); // !!! 核心
  }

  subscribe (callback) {
    this.observerList.push(callback);
  }
}
// 
const p = new MyPromise(notifyAll => {
  fs.readFile("h.js", (err, data) => {
    notifyAll(data.toString()) // resolve
  })
});

p.subscribe(data => console.log(data)); // then
複製代碼

幾行代碼就實現了一個簡單的Promise,而上面的代碼也就是把觀察者設計模式稍微改了改而已。

添加狀態

固然還沒結束,上面的MyPromise是有問題的。以前說了Promise是對異步任務的封裝,能夠當作最小異步單元(像回調同樣),而異步結果也應該只有一個,即Promise中的resolve只能使用一次,至關於EventEmitter的once事件。而上面實現的MyPromise的notifyAll是能夠用屢次的(沒有爲何),所以這就能夠產生異步任務的結果能夠不止一個的錯誤。所以解決方法就是加一個bool變量或者添加狀態即pending態和fulfilled態(本質上和一個bool變量是同樣的),當notifyAll調用一次後立馬鎖住notifyAll或者當pending態變爲fulfilled態後再次調用notifyAll函數將不起做用。

爲了和Promise對象一致,這裏使用添加狀態的方式(順便把方法名給改了一下, notifyAll => resolve, subscribe => then)。

const pending = "pending";
const fulfilled = "fulfilled";

class MyPromise {
  constructor (run) { // run 函數 (resolve) => any
    this.observerList = [];
    this.status = pending;
    const resolve = value => {
      if (this.status === pending) {
        this.status = fulfilled;
        this.observerList.forEach(callback => callback(value));
      }
    };
    run(resolve); // !!! 核心
  }

  then (callback) {
    this.observerList.push(callback);
  }
}

const p = new MyPromise(resolve => {
  setTimeout(() => {
    resolve("hello world");
    resolve("hello world2"); // 很差使了
  }, 1000);
});

p.then(value => console.log(value));複製代碼

實現鏈式調用

貌似開始有點輪廓了,不過如今的MyPromise中的then可沒有鏈式調用,接下來咱們來實現then鏈,須要注意的在Promise中then方法返回的是一個新的Promise實例不是以前的Promise。因爲then方法一直返回新的MyPromise對象,因此須要一個屬性來保存惟一的異步結果。另外一方面,在實現then方法依然是註冊回調,但實現時須要考慮當前的狀態,若是是pending態,咱們須要在返回新的MyPromise的同時,將回調註冊到隊列中,若是是fulfilled態,那直接返回新的MyPromise對象,並將上一個MyPromise對象的結果給新的MyPromise對象。

const pending = "pending";
const fulfilled = "fulfilled";

class MyPromise {
  constructor (run) { // run 函數 (resolve) => any
    this.resolvedCallback = [];
    this.status = pending;
    this.data = void 666; // 保存異步結果
    const resolve = value => {
      if (this.status === pending) {
        this.status = fulfilled;
        this.data = value; // 存一下結果
        this.resolvedCallback.forEach(callback => callback(this.data));
      }
    };
    run(resolve); // !!! 核心
  }

  then (onResolved) {
    // 這裏須要對onResolved作一下處理,當onResolved不是函數時將它變成函數
    onResolved = typeof onResolved === "function" ? onResolved : value => value;
    switch (this.status) {
      case pending: {
        return new MyPromise(resolve => {
          this.resolvedCallback.push(value => { // 再包裝
            const result = onResolved(value); // 須要判斷一下then接的回調返回的是否是一個MyPromise對象
            if (result instanceof MyPromise) {
              result.then(resolve) // 若是是,直接使用result.then後的結果,畢竟Promise裏面就須要這麼作
            } else {
              resolve(result); // 感覺一下閉包的偉大
            }
          })
        })
      }
      case fulfilled: {
        return new MyPromise(resolve => {
          const result = onResolved(this.data); // fulfilled態,this.data必定存在,其實這裏就像map過程
          if (result instanceof MyPromise) {
            result.then(resolve)
          } else {
            resolve(result); // 閉包真偉大
          }
        })
      }
    }
  }
}

const p = new MyPromise(resolve => {
  setTimeout(() => {
    resolve("hello world");
    resolve("hello world2"); // 很差使了
  }, 1000);
});

p.then(value => value + "dpf")
 .then(value => value.toUpperCase())
 .then(console.log);複製代碼

以上代碼須要重點理解,畢竟理解了上面的代碼,下面的就很容易了

錯誤處理

只有resolve和then的MyPromise對象已經完成。沒有測試的庫就是耍流氓,沒有差錯處理的代碼也是耍流氓,因此錯誤處理仍是很重要的。因爲一個異步任務可能完不成或者中間會出錯,這種狀況必須得處理。所以咱們須要加一個狀態rejected來表示異步任務出錯,而且使用rejectedCallback隊列來存儲reject發送的錯誤事件。(前方高能預警,面向try/catch編程開始了)

const pending = "pending";
const fulfilled = "fulfilled";
const rejected = "rejected"; // 添加狀態 rejected

class MyPromise {
  constructor (run) { // run 函數 (resolve, reject) => any
    this.resolvedCallback = [];
    this.rejectedCallback = []; // 添加一個處理錯誤的隊列
    this.status = pending;
    this.data = void 666; // 保存異步結果
    const resolve = value => {
      if (this.status === pending) {
        this.status = fulfilled;
        this.data = value;
        this.resolvedCallback.forEach(callback => callback(this.data));
      }
    };
    const reject = err => {
      if (this.status === pending) {
        this.status = rejected;
        this.data = err;
        this.rejectedCallback.forEach(callback => callback(this.data));
      }
    };
    try { // 對構造器裏傳入的函數進行try / catch
      run(resolve, reject); // !!! 核心
    } catch (e) {
      reject(e)
    }
  }

  then (onResolved, onRejected) { // 添加兩個監聽函數
    // 這裏須要對onResolved作一下處理,當onResolved不是函數時將它變成函數
    onResolved = typeof onResolved === "function" ? onResolved : value => value;
    onRejected = typeof onRejected === "function" ? onRejected : err => { throw err };

    switch (this.status) {
      case pending: {
        return new MyPromise((resolve, reject) => {
          this.resolvedCallback.push(value => {
            try { // 對整個onResolved進行try / catch
              const result = onResolved(value);
              if (result instanceof MyPromise) { 
                result.then(resolve, reject)
              } else {
                resolve(result); 
              }
            } catch (e) {
              reject(e) // 捕獲異常,將異常發佈
            }
          });
          this.rejectedCallback.push(err => {
            try { // 對整個onRejected進行try / catch
              const result = onRejected(err);
              if (result instanceof MyPromise) {
                result.then(resolve, reject)
              } else {
                reject(err)
              }
            } catch (e) {
              reject(err) // 捕獲異常,將異常發佈
            }
          })
        })
      }
      case fulfilled: {
        return new MyPromise((resolve, reject) => {
          try { // 對整個過程進行try / catch
            const result = onResolved(this.data);
            if (result instanceof MyPromise) {
              result.then(resolve, reject)
            } else {
              resolve(result);  
            }
          } catch (e) {
            reject(e) // 捕獲異常,將異常發佈
          }
        })
      }
      case rejected: {
        return new MyPromise((resolve, reject) => {
          try { // 對整個過程進行try / catch
            const result = onRejected(this.data);
            if (result instanceof MyPromise) {
              result.then(resolve, reject)
            } else {
              reject(result) 
            }
          } catch (e) {
            reject(e) // 捕獲異常,將異常發佈
          }
        })
      }
    }
  }
}

const p = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error("error"));
    resolve("hello world");  // 很差使了
    resolve("hello world2"); // 很差使了
  }, 1000);
});

p.then(value => value + "dpf")
 .then(console.log)
 .then(() => {}, err => console.log(err));複製代碼

能夠看出then方法的實現比較複雜,但這是一個核心的方法,實現了這個後面的其餘方法就很好實現了,下面給出MyPromise的每個方法的實現。

catch實現

這個實現很是簡單

catch (onRejected) {
    return this.then(void 666, onRejected)
}複製代碼

靜態方法MyPromise.resolve

static resolve(p) {
     if (p instanceof MyPromise) {
       return p.then()
     } 
     return new MyPromise((resolve, reject) => {
       resolve(p)
     })
 }複製代碼

靜態方法MyPromise.reject

static reject(p) {
    if (p instanceof MyPromise) {
      return p.catch()
    } 
    return new MyPromise((resolve, reject) => {
      reject(p)
    })
}
 複製代碼

靜態方法MyPromise.all

static all (promises) {
    return new MyPromise((resolve, reject) => {
      try {
        let count = 0,
            len   = promises.length,
            value = [];
        for (let promise of promises) {
          MyPromise.resolve(promise).then(v => {
            count ++;
            value.push(v);
            if (count === len) {
              resolve(value)
            }
          })
        }
      } catch (e) {
        reject(e)
      }
    });
  }複製代碼

靜態方法MyPromise.race

static race(promises) {
    return new MyPromise((resolve, reject) => {
      try {
        for (let promise of promises) {
          MyPromise.resolve(promise).then(resolve)
        }
      } catch (e) {
        reject(e)
      }
    }) 
  }複製代碼

完整的MyPromise代碼實現

const pending = "pending";
const fulfilled = "fulfilled";
const rejected = "rejected"; // 添加狀態 rejected

class MyPromise {
  constructor (run) { // run 函數 (resolve, reject) => any
    this.resolvedCallback = [];
    this.rejectedCallback = []; // 添加一個處理錯誤的隊列
    this.status = pending;
    this.data = void 666; // 保存異步結果
    const resolve = value => {
      if (this.status === pending) {
        this.status = fulfilled;
        this.data = value;
        this.resolvedCallback.forEach(callback => callback(this.data));
      }
    };
    const reject = err => {
      if (this.status === pending) {
        this.status = rejected;
        this.data = err;
        this.rejectedCallback.forEach(callback => callback(this.data));
      }
    };
    try { // 對構造器裏傳入的函數進行try / catch
      run(resolve, reject); // !!! 核心
    } catch (e) {
      reject(e)
    }
  }

  static resolve (p) {
    if (p instanceof MyPromise) {
      return p.then()
    }
    return new MyPromise((resolve, reject) => {
      resolve(p)
    })
  }

  static reject (p) {
    if (p instanceof MyPromise) {
      return p.catch()
    }
    return new MyPromise((resolve, reject) => {
      reject(p)
    })
  }

  static all (promises) {
    return new MyPromise((resolve, reject) => {
      try {
        let count = 0,
            len   = promises.length,
            value = [];
        for (let promise of promises) {
          MyPromise.resolve(promise).then(v => {
            count ++;
            value.push(v);
            if (count === len) {
              resolve(value)
            }
          })
        }
      } catch (e) {
        reject(e)
      }
    });
  }

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      try {
        for (let promise of promises) {
          MyPromise.resolve(promise).then(resolve)
        }
      } catch (e) {
        reject(e)
      }
    })
  }

  catch (onRejected) {
    return this.then(void 666, onRejected)
  }

  then (onResolved, onRejected) { // 添加兩個監聽函數
    // 這裏須要對onResolved作一下處理,當onResolved不是函數時將它變成函數
    onResolved = typeof onResolved === "function" ? onResolved : value => value;
    onRejected = typeof onRejected === "function" ? onRejected : err => { throw err };

    switch (this.status) {
      case pending: {
        return new MyPromise((resolve, reject) => {
          this.resolvedCallback.push(value => {
            try { // 對整個onResolved進行try / catch
              const result = onResolved(value);
              if (result instanceof MyPromise) { 
                result.then(resolve, reject)
              } else {
                resolve(result); 
              }
            } catch (e) {
              reject(e)
            }
          });
          this.rejectedCallback.push(err => {
            try { // 對整個onRejected進行try / catch
              const result = onRejected(err);
              if (result instanceof MyPromise) {
                result.then(resolve, reject)
              } else {
                reject(err)
              }
            } catch (e) {
              reject(err)
            }
          })
        })
      }
      case fulfilled: {
        return new MyPromise((resolve, reject) => {
          try { // 對整個過程進行try / catch
            const result = onResolved(this.data);
            if (result instanceof MyPromise) {
              result.then(resolve, reject)
            } else {
              resolve(result);  // emit
            }
          } catch (e) {
            reject(e)
          }
        })
      }
      case rejected: {
        return new MyPromise((resolve, reject) => {
          try { // 對整個過程進行try / catch
            const result = onRejected(this.data);
            if (result instanceof MyPromise) {
              result.then(resolve, reject)
            } else {
              reject(result)
            }
          } catch (e) {
            reject(e)
          }
        })
      }
    }
  }
}複製代碼

總結

本文想要從發佈-訂閱和觀察者模式分析Promise的實現,先從異步編程的演變提及,回調函數到發佈-訂閱和觀察者設計模式,而後發現Promise和觀察者設計模式比較相似,因此先從這個角度分析了Promise的實現,固然Promise的功能遠不如此,因此本文分析了Promise的經常使用方法的實現原理。Promise的出現改變了傳統的異步編程方式,使JavaScript在進行異步編程時更加靈活,代碼更加可維護、可閱讀。因此做爲一個有追求的前端,必需要對Promise的實現有必定的理解。

相關文章
相關標籤/搜索