手写 PromiseA+ 实现,轻松通过 872 条用例
手写 Promise/A+ 实现,轻松通过 872 条用例
规范参考:Promise/A+ 规范 - 中文版本
前言
从接触 Promise 到现在,笔者经历了这么个过程:
- 了解各种 Promise 规范,包括 Promise/A+,但对其具体内容不甚了解。
- 研究前人的 Promise 实现,自己尝试编写,但测试时经常遇到问题。
- 苦思冥想,为什么返回值能够决定 Promise 的状态?为什么链式调用要返回一个新的 Promise 而不是 this?我陷入了深深的困惑。
- 可能是由于执着,我深入研读了 Promise/A+ 规范。尽管起初对一些条款感到困惑,但我依然坚持学习。
- 逐句翻译、理解规范,逐渐熟悉了它。从最初需要对照规范编写代码,到如今能够手写 Promise,这个过程充满了乐趣,也让我对 Promise 的一些常用方法有了更深的理解。
如果你也在学习 Promise,遇到困难,我建议你对照规范和代码来理解。
实现
在阅读代码之前,你需要明白,代码中的注释并非随意添加,每条注释都对应着 Promise/A+ 规范中的具体条款。
关于规范,它主要包括以下几个部分:
- 专业术语,你可以简单了解即可。
- 详细规范,需要逐行理解,包括:
- 2.1 Promise 的 3 种状态:pending(待定)、fulfilled(已实现)、rejected(已拒绝)。
- 2.2 Promise 的
then方法的实现,不同状态下应执行的操作。 - 2.3 Promise 对某值的决策行为,也称为 Promise 解决过程。
因此,你会在注释中看到类似以下的标记:
// 2.1 (2)(2)
它表示,规范对应的 2.1 Promise 状态 下的第 2 个序号下的 第 2 条内容。
即 标题序号 内容序号... 。
下方代码中尽管没有涵盖所有规范条款的注释,但是隐式实现了。
完整代码如下:
function MyPromise(executor) {
this.state = 'pending' // 2.1 (1)(1) 初始状态,可以转变为其它两种状态,也就是已实现(fulfilled)或已拒绝(rejected)
this.value = null // 2.1 (2)(2) 必须有一个不可改变的值
this.reason = null // 2.1 (3)(2) 必须有一个不可改变的原因
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
if (this.state !== 'pending') return
this.state = 'fulfilled'
this.value = value
// 2.2 (6)(1) 当 promise 被实现时,所有相应的 onFulfilled 回调必须按它们调用 then 的顺序执行
this.onFulfilledCallbacks.forEach((callback) => callback(this.value))
}
const reject = (reason) => {
if (this.state !== 'pending') return
this.state = 'rejected'
this.reason = reason
// 2.2 (6)(2) 当 promise 被拒绝时,所有相应的 onRejected 回调必须按它们调用 then 的顺序执行
this.onRejectedCallbacks.forEach((callback) => callback(this.reason))
}
// 如果 then 中对返回的 Promise 执行器做了异常处理,此步可选
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
// 2.2 (6) 原型上方法,根据不同状态保证同一个 Promise 上的 then 可被多次调用
MyPromise.prototype.then = function (onFulfilled, onRejected) {
// 2.2 (1) onFulfilled 和 onRejected 参数可选,若不是函数则忽略(此处略微改造,本质上还是符合规范的,返回值或抛出异常决策链调用)
const realOnFulfilled =
typeof onFulfilled === 'function'
? onFulfilled // 2.2 (2)
: (value) => {
return value
}
const realOnRejected =
typeof onRejected === 'function'
? onRejected // 2.2 (3)
: (reason) => {
throw reason
}
// 2.2 (7) then 必须返回一个 promise
let promise2 = new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
// 2.2 (4) 必须在执行上下文栈仅包含平台代码时才被调用
queueMicrotask(() => {
try {
// 2.2 (5) 必须作为函数调用
let x = realOnFulfilled(this.value)
// 2.2 (7)(1) 根据返回值 x 运行 Promise 解决过程 [[Resolve]](promise2, x),此处也内含了规范 2.2 (7)(3) 处理
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
// 2.2 (7)(2) 抛出异常 e,则 promise2 必须以 e 作为原因被拒绝,此处也包含了 2.2 (7)(4) 的处理(非函数时上方默认函数抛出 reason,这里捕捉拒绝,不就是实现了吗)
reject(e)
}
})
} else if (this.state === 'rejected') {
queueMicrotask(() => {
try {
let x = realOnRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
} else if (this.state === 'pending') {
// 如果 Promise 中的异步执行在后,then添加在前,该步骤能保证回调不被忽略,参考观察者 or 发布订阅?
this.onFulfilledCallbacks.push(() => {
queueMicrotask(() => {
try {
let x = realOnFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
})
this.onRejectedCallbacks.push(() => {
queueMicrotask(() => {
try {
let x = realOnRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
})
}
})
// 2.2 (7)
return promise2
}
function resolvePromise(promise, x, resolve, reject) {
// 2.3 (1) 如果 promise 和 x 指向同一个对象,则以 TypeError 拒绝 promise 作为原因,这里抛出或者 return reject 均可
if (promise === x)
throw new TypeError('promise and return value are the same')
// 根据 Promise 或者 thenable 对象特性,可以直接判断分支如下
if (x !== null && (typeof x === 'function' || typeof x === 'object')) {
// 2.3 (3)(3)(3) 和 2.3 (3)(4)(1) 调用优先问题专用变量
let called = false
try {
const then = x.then
if (typeof then === 'function') {
// 2.3 (3)(3) 如果 then 是一个函数,则以 x 作为 this 调用它
then.call(
x,
(y) => {
if (called) return
called = true
// 2.3 (3)(3)(1)
resolvePromise(promise, y, resolve, reject)
},
(r) => {
if (called) return
called = true
// 2.3 (3)(3)(2)
reject(r)
}
)
} else {
// 2.3 (3)(4) x 为非 thenable 对象(如果 then 不是一个函数,则用 x 来实现 promise)
resolve(x)
}
} catch (e) {
if (called) return
called = true
// 2.3 (3)(2) 如果检索属性 x.then 时抛出异常 e,则以 e 作为原因拒绝 promise
// 2.3 (3)(3)(4)(2) 如果调用 then 时抛出异常,以 e 作为原因拒绝 promise
reject(e)
}
} else {
// 2.3 (4) 如果 x 既不是对象也不是函数,则用 x 来实现 promise
// x 为 null undefined、基本数值等情况
resolve(x)
}
}
// 暴露一个接口提供测试的静态方法
MyPromise.deferred = function () {
const result = {}
result.promise = new MyPromise((resolve, reject) => {
result.resolve = resolve
result.reject = reject
})
return result
}
module.exports = MyPromise
手写 PromiseA+ 实现,轻松通过 872 条用例的更多相关文章
- AI应用开发实战 - 手写算式计算器
扩展手写数字识别应用 识别并计算简单手写数学表达式 主要知识点 了解MNIST数据集 了解如何扩展数据集 实现手写算式计算器 简介 本文将介绍一例支持识别手写数学表达式并对其进行计算的人工智能应用的开 ...
- 手写一个Promise/A+,完美通过官方872个测试用例
前段时间我用两篇文章深入讲解了异步的概念和Event Loop的底层原理,然后还讲了一种自己实现异步的发布订阅模式: setTimeout和setImmediate到底谁先执行,本文让你彻底理解Eve ...
- ClownFish:比手写代码还快的通用数据访问层
http://www.cnblogs.com/fish-li/archive/2012/07/17/ClownFish.html 阅读目录 开始 ClownFish是什么? 比手写代码还快的执行速度 ...
- BP神经网络(手写数字识别)
1实验环境 实验环境:CPU i7-3770@3.40GHz,内存8G,windows10 64位操作系统 实现语言:python 实验数据:Mnist数据集 程序使用的数据库是mnist手写数字数据 ...
- 手写MQ框架(一)-准备启程
一.背景 很久以前写了DAO框架和MVC框架,前段时间又重写了DAO框架-GDAO(手写DAO框架(一)-从“1”开始,源码:https://github.com/shuimutong/gdao.gi ...
- 【Win 10 应用开发】手写识别
记得前面(忘了是哪天写的,反正是前些天,请用力点击这里观看)老周讲了一个14393新增的控件,可以很轻松地结合InkCanvas来完成涂鸦.其实,InkCanvas除了涂鸦外,另一个大用途是墨迹识别, ...
- stanford coursera 机器学习编程作业 exercise 3(使用神经网络 识别手写的阿拉伯数字(0-9))
本作业使用神经网络(neural networks)识别手写的阿拉伯数字(0-9) 关于使用逻辑回归实现多分类问题:识别手写的阿拉伯数字(0-9),请参考:http://www.cnblogs.com ...
- 【OpenCV】opencv3.0中的SVM训练 mnist 手写字体识别
前言: SVM(支持向量机)一种训练分类器的学习方法 mnist 是一个手写字体图像数据库,训练样本有60000个,测试样本有10000个 LibSVM 一个常用的SVM框架 OpenCV3.0 中的 ...
- MNIST手写数字数据库
手写数字库很容易建立,但是总会很浪费时间.Google实验室的Corinna Cortes和纽约大学柯朗研究所的Yann LeCun建有一个手写数字数据库,训练库有60,000张手写数字图像,测试库有 ...
- 【机器学习】BP神经网络实现手写数字识别
最近用python写了一个实现手写数字识别的BP神经网络,BP的推导到处都是,但是一动手才知道,会理论推导跟实现它是两回事.关于BP神经网络的实现网上有一些代码,可惜或多或少都有各种问题,在下手写了一 ...
随机推荐
- docker常用命令与应用
docker入门与docker file介绍 原文地址 docker常用命令 https://blog.csdn.net/leilei1366615/article/details/106267225 ...
- 学习JavaScript第四天
文章目录 1 回顾 内置对象 2 内置对象 2.1 Function 2.2 Global 3 DOM 部分知识点介绍 4 BOM 4.1 window ① 弹框 ② 打开关闭窗口 ③ 页面滚动 ④ ...
- Tony Bai · Go语言第一课 _个人笔记 04|初窥门径:一个Go程序的结构是怎样的?
Tony Bai · Go语言第一课 _个人笔记 04|初窥门径:一个Go程序的结构是怎样的? 1.配置国内的Go模块的镜像 配置国内镜像代理(使用阿里云镜像) go env -w GOPROXY=h ...
- linu管理文本文件
vi\vim编辑器的三种工作模式 命令模式(Command mode):按键编辑器都理解为命令,以命令驱动执行不同的功能.此模型下,不能自由进行文本编辑. 输入模式(Insert mode):也就是所 ...
- .NET 高性能异步套接字库,支持多协议、跨平台、高并发
前言 .NET 生态中有哪些值得推荐的网络通信框架?今天,给大家推荐一个非常优秀的开源项目--NetCoreServer. NetCoreServer 是一款 .NET 开源.免费.快速且低延迟的异步 ...
- mysql隐蔽的索引规则导致数据全表扫描
索引是为了加速数据的检索,但是不合理的表结构或适应不当则会起到反作用.我们在项目中就遇到过类似的问题,两个十万级别的数据表,在做连接查询的时候,查询时间达到了7000多秒还没有查出结果. 首先说明,关 ...
- vscode连接docker时需要为docker容器开设一个映射端口
相关: vscode远程连接远程主机上的docker -- 设置命令 -- -p 5001:5001 设置端口: -p 5001:5001 命令Demo: docker run -it -v /hom ...
- 初识cuda一文通
cuda学习博客 本文为本人cuda学习过程中的记录和理解,多参考@谭升等大佬前辈的博客,以及NVIDIA官方文档.如有错误烦请指正,如有侵权请联系删除. 0. 并行计算与计算机架构 计算机架构是并行 ...
- 题解:CF634A Island Puzzle
CF634A Island Puzzle 题解 分析 由于我们仅能移动 \(0\),所以其它数字的相对顺序较原来应该是不变的,所以我们从环中删除 \(0\) 再判断相对位置即可. 还有需要注意的是本题 ...
- Spring3.0核心组件的源码简单分析
前言 本文结合Spring3.0的源码进行简单的介绍,这里的核心组件不是我们常见所谓的IOC和AOP,而是以Spring3.0发布的开发包为切入点,当然Spring3.0以后的各个版本基本上差不多,思 ...