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)可以按照自己的需求从消息中心订阅信息,跟消息队列挺类似的。

在观察者模式只有两种组件:接收者和发布者,而发布-订阅模式中则有三种组件:发布者、消息中心和接收者。

image.png

在代码实现上的差异也比较明显

观察者设计模式

// 观察者设计模式
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的实现有一定的理解。

链接:https://juejin.im/post/6844903834184073224

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容