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

SundialDream1發表於2019-05-01

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

前言

Promise作為非同步程式設計的一種解決方案,比傳統的回撥和事件更加強大,也是學習前端所必須要掌握的。作為一個有追求的前端,不僅要熟練掌握Promise的用法,而且要對其實現原理有一定的理解(說白了,就是面試裝逼必備)。雖然網上有很多Promise的實現程式碼,幾百行的,但個人覺得,不對非同步程式設計和Promise有一定的理解,那些程式碼也就是一個板子而已(面試可不能敲板子)。首先預設讀者都對Promise物件比較熟悉了,然後將從前端最常用的設計模式:釋出-訂閱和觀察者模式的角度來一步一步的實現Promise。

從非同步程式設計說起

既然Promise是一種非同步解決方案,那麼在沒有Promise物件之前是怎麼做非同步處理的呢?有兩種方法:回撥函式和釋出-訂閱或觀察者設計模式。(關於實現非同步程式設計的更多方式請參考我的文章:JavaScript實現非同步程式設計的5種方式

回撥函式(callback function)

相信回撥函式讀者都不陌生,畢竟最早接觸的也就是回撥函式了,而且用回撥函式做非同步處理也很簡單,以nodejs檔案系統模組fs為例,讀取一個檔案一般都會這麼做

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

其缺點也很明顯,當非同步流程變得複雜,那麼回撥也會變得很複雜,有時也叫做”回撥地獄”,就以檔案複製為例

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是世界上最好的語言)

# 這程式碼屬實沒法看啊
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:手撕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的實現有一定的理解。


相關文章