如何从零开始利用js手写一个Promise库详解
作者:zach5078 发布时间:2024-04-19 10:46:32
前言
ECMAScript 是 JavaScript 语言的国际标准,JavaScript 是 ECMAScript 的实现。ES6 的目标,是使得 JavaScript 语言可以用来编写大型的复杂的应用程序,成为企业级开发语言。
概念
ES6 原生提供了 Promise 对象。
所谓 Promise,就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理。
三道思考题
刚开始写前端的时候,处理异步请求经常用callback,简单又顺手。后来写着写着就抛弃了callback,开始用promise来处理异步问题。promise写起来确实更加优美,但由于缺乏对它内部结构的深刻认识,每次在遇到一些复杂的情况时,promise用起来总是不那么得心应手,debug也得搞半天。
所以,这篇文章我会带大家从零开始,手写一个基本能用的promise。跟着我写下来以后,你会对promise是什么以及它的内部结构有一个清楚的认知,未来在复杂场景下使用promise也能如鱼得水。
而且,为了检验大家是否真的完全掌握了promise,我会在文章结尾出几道跟promise相关的练习题。说是练习题,其实都是大家项目中会遇到的真实场景的抽象,熟练掌握可以帮助大家在前端方面更上一层楼。
提前将三道练习题给出来,大家可以先不看下文的内容,在脑海里大概构思下你会怎么解决:
promise array的链式调用?
promise怎么做并发控制?
promise怎么做异步缓存?
以上三道思考题其实跟你用不用promise并没有多大关系,但是如果你不深刻理解promise想要解决这三个问题还真不是那么轻松的。
什么是Promise
回到正文,什么是Promise?说白了,promise就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
首先,ES6规定Promise对象是一个构造函数,用来生成Promise实例。然后,这个构造函数接受一个函数(executor)作为参数,该函数的两个参数分别是resolve和reject。最后,Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数(onFulfilled和onRejected)。
具体的使用方法,用代码表现是这样:
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(error) {
// failure
});
理解了这个后,我们就可以大胆的开始构造我们自己的promise了,我们给它取个名字:CutePromise
实现一个Promise:CutePromise
我们直接用ES6的class来创建我们的CutePromise,对ES6语法还不熟悉的,可以先读一下我的另外两篇介绍ES6核心语法的文章后再回来。30分钟掌握ES6/ES2015核心内容(上) 、30分钟掌握ES6/ES2015核心内容(下)
class CutePromise {
// executor是我们实例化CutePromise时传入的参数函数,它接受两个参数,分别是resolve和reject。
// resolve和reject我们将会定义在constructor当中,供executor在执行的时候调用
constructor(executor) {
const resolve = () => {}
const reject = () => {}
executor(resolve, reject)
}
// 为实例提供一个then的方法,接收两个参数函数,
// 第一个参数函数必传,它会在promise已成功(fulfilled)以后被调用
// 第二个参数非必传,它会在promise已失败(rejected)以后被调用
then(onFulfilled, onRejected) {}
}
创建了我们的CutePromise后,我们再来搞清楚一个关键点:Promise 对象的状态。
Promise 对象通过自身的状态,来控制异步操作。一个Promise 实例具有三种状态:
异步操作未完成(pending)
异步操作成功(fulfilled)
异步操作失败(rejected)
上面三种状态里面,fulfilled和rejected合在一起称为resolved(已定型)。状态的切换只有两条路径:第一种是从pending=>fulfilled,另一种是从pending=>rejected,状态一旦切换就不能再改变。
现在我们来为CutePromise添加状态,大概流程就是:
首先,实例化初始过程中,我们先将状态设为PENDING,然后当executor执行resolve的时候,将状态更改为FULFILLED,当executor执行reject的时候将状态更改为REJECTED。同时更新实例的value。
constructor(executor) {
...
this.state = 'PENDING';
...
const resolve = (result) => {
this.state = 'FULFILLED';
this.value = result;
}
const reject = (error) => {
this.state = 'REJECTED';
this.value = error;
}
...
}
再来看下我们的then函数。then函数的两个参数,onFulfilled表示当promise异步操作成功时调用的函数,onRejected表示当promise异步操作失败时调用的函数。假如我们调用then的时候,promise已经执行完成了(当任务是个同步任务时),我们可以直接根据实例的状态来执行相应的函数。假如promise的状态还是PENDING, 那我们就将onFulfilled和onRejected直接存储到chained这个变量当中,等promise执行完再调用。
constructor(executor) {
...
this.state = 'PENDING';
// chained用来储存promise执行完成以后,需要被依次调用的一系列函数
this.chained = [];
const resolve = (result) => {
this.state = 'FULFILLED';
this.value = result;
// promise已经执行成功了,可以依次调用.then()函数里的onFulfilled函数了
for (const { onFulfilled } of this.chained) {
onFulfilled(res);
}
}
...
}
then(onFulfilled, onRejected) {
if (this.state === 'FULFILLED') {
onFulfilled(this.value);
} else if (this.state === 'REJECTED') {
onRejected(this.value);
} else {
this.$chained.push({ onFulfilled, onRejected });
}
}
这样我们就完成了一个CutePromise的创建,下面是完整代码,大家可以复制代码到控制台测试一下:
class CutePromise {
constructor(executor) {
if (typeof executor !== 'function') {
throw new Error('Executor must be a function');
}
this.state = 'PENDING';
this.chained = [];
const resolve = res => {
if (this.state !== 'PENDING') {
return;
}
this.state = 'FULFILLED';
this.internalValue = res;
for (const { onFulfilled } of this.chained) {
onFulfilled(res);
}
};
const reject = err => {
if (this.state !== 'PENDING') {
return;
}
this.state = 'REJECTED';
this.internalValue = err;
for (const { onRejected } of this.chained) {
onRejected(err);
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
if (this.state === 'FULFILLED') {
onFulfilled(this.internalValue);
} else if (this.$state === 'REJECTED') {
onRejected(this.internalValue);
} else {
this.chained.push({ onFulfilled, onRejected });
}
}
}
提供一下测试代码:
let p = new CutePromise(resolve => {
setTimeout(() => resolve('Hello'), 100);
});
p.then(res => console.log(res));
p = new CutePromise((resolve, reject) => {
setTimeout(() => reject(new Error('woops')), 100);
});
p.then(() => {}, err => console.log('Async error:', err.stack));
p = new CutePromise(() => { throw new Error('woops'); });
p.then(() => {}, err => console.log('Sync error:', err.stack));
来源:https://segmentfault.com/a/1190000014440641


猜你喜欢
- 一年一度的双十一就快到了,各种砍价、盖楼、挖现金的口令将在未来一个月内充斥朋友圈、微信群中。玩过多次双十一活动的小编表示一顿操作猛如虎,一看
- 目的检测字符串中是否包含某字符集合中的字符方法 最简洁的方法如下,清晰,通用,快速,适用于任何序列和容器def containAny(seq
- spark编程python实例ValueError: Cannot run multiple SparkContexts at once;
- 一、绘制折线图使用plot()绘制折线图常用的参数:x:表示x轴的数据y:表示y轴的数据fmt:表示快速设置条样式的格式字符串。label:
- 前言 网传的七天学Python的路线如下,我觉得可以在学过此表中前几天的内容后,就可以回头来学习一下列表推导式:它综合了列表、fo
- Access保留字&变量名列表,建表时应避免使用这些词汇和符号。Access 2002/2003-A &nbs
- 目录实验环境依赖项安装编程实现浏览器有一个可以用于展示网页的窗口代码总结实验环境操作系统:Linux Mint编辑器:vim编程语言:pyt
- 经常使用到有关数据库的操作。包括连接代码、SQL命令等等,又不曾刻意去记忆它们(我本人是不愿意去记这东东),所以常常在用到的时候又去查书本,
- argparse模块用法一、 概念argsparse是python的命令行解析的标准模块,内置于python,不需要安装。这个库可以让我们直
- 前言相关性分析算是很多算法以及建模的基础知识之一了,十分经典。关于许多特征关联关系以及相关趋势都可以利用相关性分析计算表达。其中常见的相关性
- 前言索引(index)是帮助MySQL高效获取数据的数据结构。 它对于高性能非常关键,但人们通常会忘记或误解它。 索引在数据越大的时候越重要
- 目录1安装loguru|2loguru简单使用|3loguru保留日志文件|4loguru字符串输出|5loguru封装类,可以直接拿去用!
- 一、报错信息:【file】【Default Settint】---Project Interpreter 点击搜索suds安装模块报错解决:
- 背景:由于需要对ocr识别系统的表格识别结果做验证,通过返回的json文件结果对比比较麻烦,故需要将json文件里面的识别结果还原为表格做验
- 使用mysql 中的load 命令,讲txt 文件中的内容加载到数据库表中,例如,创建table,名称是user,一个字段username;
- 1、配置安装源# 安装dnf install http://mirrors.ustc.edu.cn/mysql-repo/mysql80-c
- 众所周知,FileSystemObject(fso)组件的强大功能及破坏性是它屡屡被免费主页提供商(那些支持ASP)的禁用的原因,我整理了一
- 之前服务器上配置测试用的服务环境,我偷懒顺手用网上现成的脚本进行安装,结果MySQL启动不了,于是我只有老老实实的重新安装MySQL Ser
- 关于英文的写作有一本十分著名的书,The Elements of Style(风格要素),编写程序也有一本The Elements of P
- 对Vue全家桶有基本的认知.用有node环境了解express一丶业务分析1.什么情况下进行权限验证?访问敏感接口前端向后端敏感接口发送aj