js手写Promise(上)
目录
- 构造函数
- resolve与reject
- 状态改变
- 状态改变后就无法再次改变
- 代码优化
- 回调函数中抛出错误
- then
- onFulfilled和onRejected的调用时机
- 异步then
- 多个then
如果是不知道或者对Promise
不熟悉的铁铁可以先看我这篇文章
Promise
构造函数
在最开始,我们先不去考虑Promise
内部是怎么实现,而是先将自己的Promise
声明出来,这里我使用ES6
的class
来声明
class MyPromise {}
在我们new
一个Promise
的时候会传入一个回调函数
,这个回调函数有两个形参
,一个resolve
,一个reject
,这个函数将交给Promise
来立即执行
,所以我们的constructor
可以这么写
class MyPromise {constructor(func) {func(resolve, reject)}
}
值得注意的是,Promise
本身就是一个任务
,而回调函数
表示的是任务的执行过程
,所以constructor
中的形参
应该叫executor
而不是func
class MyPromise {constructor(executor) {executor(resolve, reject)}
}
resolve与reject
resolve
和reject
也是函数,那么这两个函数定义在哪呢,有2种方案
-
定义在
constructor
中constructor(executor) {const resolve = (data) => {}const reject = (reason) => {}executor(resolve, reject) }
-
将其变为
原型方法
class MyPromise { constructor(executor) {func(this.#resolve, this.#reject) } #reject(reason) { } #resolve(data) { } }
因为这个函数我们只会在类的内部使用,并不希望用户能在外部访问,所以我们将它定义为
私有成员
只不过这么写的话会有this
的指向问题,我们需要使用强制绑定
来将函数绑定到正确的地方class MyPromise { constructor(executor) {func(this.#resolve.call(this), this.#reject.call(this)) } #reject(reason) { } #resolve(data) { } }
这里我选择第一种方法
状态改变
现在我们声明了resolve
与reject
两个函数,但具体这两个函数做什么我们并不清楚,事实上这两个函数做的都是同一件事,改变当前Promise实例的状态与值
,只不过resolve
是将当前实例的状态改为fulfilled
,而reject
是将当前实例的状态改为rejected
,明白了这一点我们就能写出如下代码
class MyPromise {#state = "pending"#value = nullconstructor(executor) {const resolve = (data) => {this.#state = "fulfilled"this.#value = data}const reject = (reason) => {this.#state = "rejected"this.#value = reason}executor(resolve, reject)}
}
我们声明了两个私有属性
,无论是state
还是value
我们都不希望用户能从外部访问,state
用于记录当前实例的状态
,而value
用于记录当前实例得到的值
状态改变后就无法再次改变
这么写就完了吗?当然没有,在Promise
中状态一旦确定就不能再更改
,反映到代码层面就是无论是在回调函数中写多少个resolve
和reject
,Promise
都只会执行第一个,而我们的Promise
中目前并没有实现这个功能
const resolve = (data) => {if (this.#state !== "pending") returnthis.#state = "fulfilled"this.#value = data
}
const reject = (reason) => {if (this.#state !== "pending") returnthis.#state = "rejected"this.#value = reason
}
我们在resolve
和reject上
都加了一行判断,如果当前实例的state
不是pending
的话就说明状态已经改变,不能再继续执行
写到这里我们发现resolve
和reject
函数中的重复代码有点多,所以我们可以将其封装成一个独立的函数
class MyPromise {#state = "pending"#value = nullconstructor(executor) {const resolve = (data) => {this.#changeState("fulfilled", data)}const reject = (reason) => {this.#changeState("rejected", reason)}executor(resolve, reject)}#changeState(state, value) {if (this.#state !== "pending") returnthis.#state = statethis.#value = value}
}
代码优化
现在我们发现在我们的代码中还存在着一些硬编码
的部分,如状态不应该直接使用字符串而是需要使用变量存起来,这样如果以后状态的名称发生改变,我们也就只需要更改变量的内容
class MyPromise {#state = "pending"#value = nullstatic #PENDING = "pending"static #FULFILLED = "fulfilled"static #REJECTED = "rejected"constructor(executor) {const resolve = (data) => {this.#changeState(MyPromise.#FULFILLED, data)}const reject = (reason) => {this.#changeState(MyPromise.#REJECTED, reason)}executor(resolve, reject)}#changeState(state, value) {if (this.#state !== MyPromise.#PENDING) returnthis.#state = statethis.#value = value}
}
我们将三种状态用变量存起来,因为三个状态只会在内部使用而且每个实例都会拥有这三个状态,所以我将其定义为静态私有成员
回调函数中抛出错误
现在大部分问题我们都解决了,但是在回调函数中抛出错误
的情况我们并没有处理,在Promise
中如果回调函数中抛出了错误会被Promise内部捕获到
,直接reject
,那么我们的代码就可以这么写
constructor(executor) {const resolve = (data) => {this.#changeState(MyPromise.#FULFILLED, data)}const reject = (reason) => {this.#changeState(MyPromise.#REJECTED, reason)}try {executor(resolve, reject)} catch (error) {reject(error)}
}
至此我们就将MyPromise
的构造器
部分完成了
then
在PromiseA+
规范中通篇都在说什么是Promise
,简单地说就是Promise
可以是一个对象
或者是函数
,但无论是什么都必须要有then
方法,如果有then
方法那就是Promise
所以then
方法是Promise
中的核心,同时也是手写Promise
中最难的一部分,如果能将then
方法手写出来那整个Promise
就可以算是大部分完成了
我们回忆一下Promise
中的then
方法,发现then
方法会传入两个参数,一个是成功时的回调函数
,一个是失败时的回调函数
,那我们可以这么定义
class MyPromise {then(onFulfilled, onRejected) {return new MyPromise((resolve, reject) => {})}
}
因为then
方法是每个实例都拥有并且用到的,所以我们将其定义为成员方法
,为了实现Promise的链式调用
所以then
方法必须返回一个Promise
,那么在这个返回的Promise
中,我们究竟该做些什么呢
onFulfilled和onRejected的调用时机
onFulfilled
和onRejected
什么时候调用,这个问题很好解决,依据当前Promise
的状态判断是调用onFulfilled
还是onRejected
then(onFulfilled, onRejected) {return new MyPromise((resolve, reject) => {if (this.#state === MyPromise.#FULFILLED) onFulfilled(this.#value)if (this.#state === MyPromise.#REJECTED) onRejected(this.#value)})
}
这么写似乎并没有什么问题,那我们来测试一下
let p1 = new MyPromise((resolve, reject) => {resolve(123)
})
let p2 = new MyPromise((resolve, reject) => {setTimeout(() => {resolve(456)}, 1000)
})
p1.then(data => {console.log(data)
})
p2.then(data => {console.log(data)
})
看得出来,p1
成功运行了,但p2
似乎有点问题,因为p2
在运行到then
的时候p2
的状态还是pending
,p2
的状态会在一秒钟后才改变,但then
方法早在这之前就调用了,所以为了避免这种情况,我们需要在状态改变的时候再次调用then方法
异步then
再次调用then方法
说起来并不精确,我们其实真正想要的并不是调用then
方法,而是想要在状态改变的时候调用onFulfiled
或者onRejected
,那么第一个问题就来了,我们在哪里能知道状态什么时候被改变了?答案是changeState
changeState
是用来改变当前实例的状态的函数,当它第一次运行时状态肯定被改变,我们只需要在这里调用onFulfilled
或者onRejected
,但是有一个新问题,这两个回调函数都是直接传入then
中的,我们无法在changeState中拿到这两个函数
,那该怎么办呢?我们可以用一个中间变量存储
class MyPromise {#handler = {}#changeState(state, value) {if (this.#state !== MyPromise.#PENDING) returnthis.#state = statethis.#value = valueif (this.#state === MyPromise.#FULFILLED) this.#handler.onFulfilled(this.#value)else if (this.#state === MyPromise.#REJECTED) this.#handler.onRejected(this.#value)}then(onFulfilled, onRejected) {return new MyPromise((resolve, reject) => {if (this.#state === MyPromise.#FULFILLED) onFulfilled(this.#value)else if (this.#state === MyPromise.#REJECTED) onRejected(this.#value)else this.#handler = {onFulfilled,onRejected,resolve,reject}})}
}
这样问题就解决了,但这里面的重复代码有点多,我们可以将其封装成一个函数
class MyPromise {#changeState(state, value) {if (this.#state !== MyPromise.#PENDING) returnthis.#state = statethis.#value = valuethis.#run()}then(onFulfilled, onRejected) {return new MyPromise((resolve, reject) => {this.#handler = {onFulfilled,onRejected,resolve,reject}this.#run()})}#run() {if (this.#state === MyPromise.#FULFILLED) {this.#handler.onFulfilled(this.#value)}else if (this.#state === MyPromise.#REJECTED) {this.#handler.onRejected(this.#value)}}}
我们封装了一个run
函数,这个函数专门用来执行then
的回调,我们还是用上面那个代码测试
至此异步then
问题解决
多个then
有时我们会在一个实例上多次调用then
方法,在实例的状态改变后这些then
方法的回调函数应该继续执行,但我们的代码却并没有实现
多个then
就意味着handler
不是一个对象
而是一个数组
,run
方法也不再调用一个handler
,而是遍历handlers
,将对应状态的回调函数全都取出来执行
class MyPromise {#handlers = []then(onFulfilled, onRejected) {return new MyPromise((resolve, reject) => {this.#handlersPush(onFulfilled, onRejected, resolve, reject)this.#run()})}#run() {if (this.#state === MyPromise.#PENDING) returnwhile (this.#handlers.length > 0) {const handler = this.#handlers.shift()if (this.#state === MyPromise.#FULFILLED) {handler.onFulfilled(this.#value)}else if (this.#state === MyPromise.#REJECTED) {handler.onRejected(this.#value)}}}#handlersPush(onFulfilled, onRejected, resolve, reject) {this.#handlers.push({onFulfilled,onRejected,resolve,reject})}
}
我们封装了一个辅助函数用于向handlers
放入回调,在run
中我们会一直在handlers
里取出回调执行,我们使用以下代码测试
let p1 = new MyPromise((resolve, reject) => {resolve(123)
})
p1.then(data => {console.log("第一个then" + data)
})
p1.then(data => {console.log("第二个then" + data)
})
至此,我们的Promise
如下
class MyPromise {#state = "pending"#value = nullstatic #PENDING = "pending"static #FULFILLED = "fulfilled"static #REJECTED = "rejected"#handlers = []constructor(executor) {const resolve = (data) => {this.#changeState(MyPromise.#FULFILLED, data)}const reject = (reason) => {this.#changeState(MyPromise.#REJECTED, reason)}try {executor(resolve, reject)} catch (error) {reject(error)}}#changeState(state, value) {if (this.#state !== MyPromise.#PENDING) returnthis.#state = statethis.#value = valuethis.#run()}then(onFulfilled, onRejected) {return new MyPromise((resolve, reject) => {this.#handlersPush(onFulfilled, onRejected, resolve, reject)this.#run()})}#run() {if (this.#state === MyPromise.#PENDING) returnwhile (this.#handlers.length > 0) {const handler = this.#handlers.shift()if (this.#state === MyPromise.#FULFILLED) {handler.onFulfilled(this.#value)}else if (this.#state === MyPromise.#REJECTED) {handler.onRejected(this.#value)}}}#handlersPush(onFulfilled, onRejected, resolve, reject) {this.#handlers.push({onFulfilled,onRejected,resolve,reject})}
}
因为内容过多,所以我将文章分为两篇,接下来的部分请看我的这篇文章
js手写Promise(下)
相关文章:

js手写Promise(上)
目录 构造函数resolve与reject状态改变状态改变后就无法再次改变 代码优化回调函数中抛出错误 thenonFulfilled和onRejected的调用时机异步then多个then 如果是不知道或者对Promise不熟悉的铁铁可以先看我这篇文章 Promise 构造函数 在最开始,我们先不去考虑Promi…...
基于Web技术的家居室内温湿度监测系统
设计一个基于Web技术的家居室内温湿度监测系统涉及前端和后端开发,以及与硬件传感器的集成。以下是一个简单的设计概述: ### 1. 系统架构 - **前端**: 用户界面,用于显示实时数据和历史记录,可通过Web浏览器访问。 - **后端**: 服…...

ubuntu22.04@laptop OpenCV Get Started: 009_image_thresholding
ubuntu22.04laptop OpenCV Get Started: 009_image_thresholding 1. 源由2. image_thresholding应用Demo2.1 C应用Demo2.2 Python应用Demo 3. 重点分析3.1 Binary Thresholding ( THRESH_BINARY )3.2 Inverse-Binary Thresholding ( THRESH_BINARY_INV )3.3 Truncate Threshold…...
Zeek实战—快速构建流量安全能力
第1章 网络流量与网络安全 1.2流量与网络 从宏观角度进行观察,如果将计算机网络看作一个整体,可以很容易抽象出它是由以下3个部分组成的。 1.网络终端。指连接在网络中的、能够产生或消费网络流量的软/硬件系统,是网络流量在正常情况下的…...
vim命令编辑完文件后,按ESC键退出编辑模式,无法进入命令模式解决方案
发现问题 在Vim编辑器中,我们通常需要按Esc键来退出编辑模式并进入命令模式。但有时,你可能会发现即使按了Esc键,也无法进入命令模式。这可能是由于某些设置或插件导致的。不过,有一个解决办法可以帮助你解决这个问题。 解决办法…...
【生产实测有效】Linux磁盘清理常用命令
经常遇到磁盘空间告警需要清理 常用方法 磁盘空间分析 先查看整体磁盘空间使用情况 df -Th lsblk 再有针对性的查看使用率过高的磁盘 du -hsx --exclude/{proc,sys,dev,boot,home,tmp,usr,var,app,ncltybbpo} /*查找大文件 find . -type d -exec tar -cjvf {}.tar.bz2 {…...
练习:鼠标类设计之1_类内容解析
前言 光做理论上的总结,不做练习理解不会那么深刻 做类的练习,解析类里面的内容有哪些 引入 电脑使用最频繁的两个外设:鼠标和键盘,他们每时每刻都在和用户交互,试做一个鼠标类 思路 我们现在要做一个鼠标类,这个类是属于能动类还是资源类呢?鼠标似乎自己做不了什么,需要和其…...

消息队列RabbitMQ-使用过程中面临的问题与解决思路
消息队列在使用过程中会出现很多问题 首先就是消息的可靠性,也就是消息从发送到消费者接收,消息在这中间过程中可能会丢失 生产者到交换机的过程、交换机到队列的过程、消息队列中、消费者接收消息的过程中,这些过程中消息都可能会丢失。 …...

搜索Agent方案
为啥需要整体方案,直接调用搜索接口取Top1返回不成嘛?要是果真如此Simple&Naive,New Bing岂不是很容易复刻->.-> 我们先来看个例子,前一阵火爆全网的常温超导技术,如果想回答LK99哪些板块会涨,你…...

排序算法---计数排序
原创不易,转载请注明出处。欢迎点赞收藏~ 计数排序(Counting Sort)是一种线性时间复杂度的排序算法,其核心思想是通过统计待排序元素的个数来确定元素的相对位置,从而实现排序。 具体的计数排序算法步骤如下ÿ…...

STM32——LCD(1)认识
目录 一、初识LCD 1. LCD介绍 2. 显示器的分类 3. 像素 4. LED和OLED显示器 5. 显示器的基本参数 (1)像素 (2)分辨率 (3)色彩深度 (4)显示器尺寸 (5ÿ…...

iTop-4412 裸机程序(二十二)- RTC时钟
目录 0.源码1. RTC2. iTop4412 中的 RTC使用的相关寄存器3. BCD编码4. 关键源码 0.源码 GitHub:https://github.com/Kilento/4412NoOS 1. RTC RTC是实时时钟(Real Time Clock)的缩写,是一种用于计算机系统的硬件设备࿰…...
Kafka 之 AdminClient API
目录 一. 前言 二. KafkaAdminClient API 2.1. API 总览 2.2. Topic 操作 2.2.1. 创建 Topic 2.2.2. Topic 列表 2.2.3. 删除 Topic 2.2.4. 描述 Topic 详细信息 2.3. 分区 Partition 操作 2.3.1. 增加分区 2.3.2. 分区副本重新分配 2.3.3. 查询分区副本列表 2.4.…...
Flutter run 一直 Running Gradle task ‘assembleDebug’…
发生缘由 Flutter 项目引入 fluttertoast 插件后,执行 Flutter run 一直 Running Gradle task ‘assembleDebug’…,最后发现下载 kotlin-compiler-embeddable-7.1.0.jar 特别的缓慢。 运行环境 电脑系统版本:Windows 10 64bit VS Code&…...

kali无线渗透之用wps加密模式破解出wpa模式的密码12
WPS(Wi-Fi Protected Setup,Wi-Fi保护设置)是由Wi-Fi联盟推出的全新Wi-Fi安全防护设定标准。该标准推出的主要原因是为了解决长久以来无线网络加密认证设定的步骤过于繁杂之弊病,使用者往往会因为步骤太过麻烦,以致干脆不做任何加密安全设定&…...

【Python】高级数据类型
🚩 WRITE IN FRONT 🚩 🔎 介绍:"謓泽"正在路上朝着"攻城狮"方向"前进四" 🔎🏅 荣誉:2021|2022年度博客之星物联网与嵌入式开发TOP5|TOP4、2021|2222年获评…...

挑战杯 python区块链实现 - proof of work工作量证明共识算法
文章目录 0 前言1 区块链基础1.1 比特币内部结构1.2 实现的区块链数据结构1.3 注意点1.4 区块链的核心-工作量证明算法1.4.1 拜占庭将军问题1.4.2 解决办法1.4.3 代码实现 2 快速实现一个区块链2.1 什么是区块链2.2 一个完整的快包含什么2.3 什么是挖矿2.4 工作量证明算法&…...

如何给最小化安装的CentOS主机装个远程桌面?
正文共:888 字 18 图,预估阅读时间:1 分钟 前面我们领微软云Azure的免费主机时(白嫖党618福利!来Azure领200美刀!外加云主机免费用一年!),发现“有资格免费试用服务”的主…...

知识图谱:py2neo将csv文件导入neo4j
文章目录 安装py2neo创建节点-连线关系图导入csv文件删除重复节点并连接边 安装py2neo 安装python中的neo4j操作库:pip install py2neo 安装py2neo后我们可以使用其中的函数对neo4j进行操作。 图数据库Neo4j中最重要的就是结点和边(关系)&a…...

备战蓝桥杯---图论之最短路Bellman-Ford算法及优化
目录 上次我们讲到复杂度为(nm)logm(m为边,n为点)的迪杰斯特拉算法,其中有一个明显的不足就是它无法解决包含负权边的图。 于是我们引进Bellman-Ford算法。 核心:枚举所有的点,能松弛就松弛,直…...

Docker 离线安装指南
参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性,不同版本的Docker对内核版本有不同要求。例如,Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本,Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...

【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...

[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?
论文网址:pdf 英文是纯手打的!论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误,若有发现欢迎评论指正!文章偏向于笔记,谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…...

Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具
文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...

Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...

ServerTrust 并非唯一
NSURLAuthenticationMethodServerTrust 只是 authenticationMethod 的冰山一角 要理解 NSURLAuthenticationMethodServerTrust, 首先要明白它只是 authenticationMethod 的选项之一, 并非唯一 1 先厘清概念 点说明authenticationMethodURLAuthenticationChallenge.protectionS…...
VTK如何让部分单位不可见
最近遇到一个需求,需要让一个vtkDataSet中的部分单元不可见,查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行,是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示,主要是最后一个参数,透明度…...

IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)
文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...

Mac下Android Studio扫描根目录卡死问题记录
环境信息 操作系统: macOS 15.5 (Apple M2芯片)Android Studio版本: Meerkat Feature Drop | 2024.3.2 Patch 1 (Build #AI-243.26053.27.2432.13536105, 2025年5月22日构建) 问题现象 在项目开发过程中,提示一个依赖外部头文件的cpp源文件需要同步,点…...

企业如何增强终端安全?
在数字化转型加速的今天,企业的业务运行越来越依赖于终端设备。从员工的笔记本电脑、智能手机,到工厂里的物联网设备、智能传感器,这些终端构成了企业与外部世界连接的 “神经末梢”。然而,随着远程办公的常态化和设备接入的爆炸式…...