从设计模式角度分析Promise:手撕Promise并不难
栏目: JavaScript · 发布时间: 6年前
内容简介: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
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
HTML Dog
Patrick Griffiths / New Riders Press / 2006-11-22 / USD 49.99
For readers who want to design Web pages that load quickly, are easy to update, accessible to all, work on all browsers and can be quickly adapted to different media, this comprehensive guide represen......一起来看看 《HTML Dog》 这本书的介绍吧!