从设计模式角度分析Promise:手撕Promise并不难
栏目: JavaScript · 发布时间: 5年前
内容简介:Promise作为异步编程的一种解决方案,比传统的回调和事件更加强大,也是学习前端所必须要掌握的。作为一个有追求的前端,不仅要熟练掌握Promise的用法,而且要对其实现原理有一定的理解(说白了,就是面试装逼必备)。虽然网上有很多Promise的实现代码,几百行的,但个人觉得,不对异步编程和Promise有一定的理解,那些代码也就是一个板子而已(面试可不能敲板子)。首先默认读者都对Promise对象比较熟悉了,然后将从前端最常用的设计模式:发布-订阅和观察者模式的角度来一步一步的实现Promise。既然Pr
前言
Promise作为异步编程的一种解决方案,比传统的回调和事件更加强大,也是学习前端所必须要掌握的。作为一个有追求的前端,不仅要熟练掌握Promise的用法,而且要对其实现原理有一定的理解(说白了,就是面试装逼必备)。虽然网上有很多Promise的实现代码,几百行的,但个人觉得,不对异步编程和Promise有一定的理解,那些代码也就是一个板子而已(面试可不能敲板子)。首先默认读者都对Promise对象比较熟悉了,然后将从前端最常用的设计模式:发布-订阅和观察者模式的角度来一步一步的实现Promise。
从异步编程说起
既然Promise是一种异步解决方案,那么在没有Promise对象之前是怎么做异步处理的呢?有两种方法:回调函数和发布-订阅或观察者设计模式。
- 回调函数(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) 可以按照自己的需求从消息中心订阅信息,跟消息队列挺类似的 。
在观察者模式只有两种组件:接收者和发布者,而发布-订阅模式中则有三种组件:发布者、消息中心和接收者。
在代码实现上的差异也比较明显
观察者设计模式
// 观察者设计模式 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的实现有一定的理解。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 从设计的角度看 Redux
- 从设计师和开发的角度使用 lottie
- 要编写优秀代码,请从设计模式始
- 从设计,页面,逻辑,走一走权限管理的完整流程
- 「可视化搭建系统」——从设计到架构,探索前端的领域和意义
- 个人角度阐述 OKR
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
数据挖掘导论
(美)Pang-Ning Tan、Michael Steinbach、Vipin Kumar / 机械工业出版社 / 2010-9 / 59.00元
本书全面介绍了数据挖掘的理论和方法,着重介绍如何用数据挖掘知识解决各种实际问题,涉及学科领域众多,适用面广。 书中涵盖5个主题:数据、分类、关联分析、聚类和异常检测。除异常检测外,每个主题都包含两章:前面一章讲述基本概念、代表性算法和评估技术,后面一章较深入地讨论高级概念和算法。目的是使读者在透彻地理解数据挖掘基础的同时,还能了解更多重要的高级主题。 本书特色 ·包含大量的图表、......一起来看看 《数据挖掘导论》 这本书的介绍吧!