面试官:请手写一个Promise

前端面试题库 (面试必备)            推荐:★★★★★

地址:前端面试题库

前言

面试官:请手写一个Promise?(开门见山)

我:既然说到Promise,那我肯定得先介绍一下JavaScript异步编程的发展史吧,这样就理解为啥Promise会出现以及Promise解决了什么问题了吧。

  • 阶段一:回调函数
  • 阶段二:事件发布/订阅模型
  • ...

面试官:我不关心什么异步编程发展史(不耐烦),这年头谁都知道Promise是解决了回调地狱的问题,我关心的是你的编码能力,你直接show you code,直接写!!!

我:好吧!(其实手写代码才是我的强项,嘻嘻!)

手写promise

先说下promise的三种状态:

  • PENDING:等待态,promise的初始状态
  • FULFILLED:成功态,promise调用resolve函数后即会从PENDING等待态变为FULFILLED成功态
  • REJECTED:失败态:promise调用reject函数后即会从PENDING等待态变为REJECTED失败态

注意:

  1. promise的状态一旦发生变更,便无法再更改。比如调用resolvePEDING变为FULFILLED,它的状态就永远是FULFILLED了,再调用reject也无法从FULFILLED变成REJECTED
  2. 状态只能从PENDING变为FULFILLEDREJECTED,不能从FULFILLEDREJECTED返回到PENDING,这个也很好理解,状态只能前进不能倒退。

先看用法:

const p = new Promise((resolve, reject) => {resolve(111);
})
p.then((value) => {console.log(value)
}, (error) => {console.log(error)
})

首先,Promise肯定是一个类,所以我们才可以new它,然后Promise实例化的时候给它传入一个回调我们叫它executor方法,Promise内部会立即调用这个executor方法,并且会传入resolvereject两个函数作为调用参数,另外在Promise类的原型上应该提供一个then方法,它里面可以传入两个回调,分别为Promise成功的回调Promise失败的回调。调用resolve后会走入成功的回调中,调用reject后会走入失败的回调中

const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'class Promise {constructor(executor) {this.value = undefinedthis.reason = undefinedthis.status = PENDINGconst resolve = (value) => {if (this.status === PENDING) {this.value = valuethis.status = FULFILLEDthis.onResolvedCallbacks.forEach(fn => fn())}}const reject = (reason) => {if (this.status === PENDING) {this.reason = reasonthis.status = REJECTEDthis.onRejectedCallbacks.forEach(fn => fn())}}executor(resolve, reject);}then(onFulfilled, onRejected) {if (this.status === FULFILLED) {onFulfilled && onFulfilled(this.value)}if (this.status === REJECTED) {onRejected && onRejected(this.reason)}}
}module.exports = Promise;

面试官:如果是异步调用resovle或者reject呢?

我:简单,用两个数组充当队列把then里边的回调存起来不就好了。


class Promise {constructor(executor) {// ...// 定义两个数组this.onResolvedCallbacks = [];this.onRejectedCallbacks = [];const resolve = (value) => {if (this.status === PENDING) {this.value = valuethis.status = FULFILLEDthis.onResolvedCallbacks.forEach(fn => fn())}}const reject = (reason) => {if (this.status === PENDING) {this.reason = reasonthis.status = REJECTEDthis.onRejectedCallbacks.forEach(fn => fn())}}// 默认执行executor函数,并传入resolve和reject函数executor(resolve, reject)}then(onFulfilled, onRejected) {if (this.status === FULFILLED) {onFulfilled && onFulfilled(this.value)}if (this.status === REJECTED) {onRejected && onRejected(this.reason)}if (this.status === PENDING) {this.onResolvedCallbacks.push(() => {onFulfilled(this.value)})this.onRejectedCallbacks.push(() => {onRejected(this.reason)})}}
}

这里定义了两个数组onResolvedCallbacksonRejectedCallbacks分别存储 then 里面成功的回调失败的回调,然后再调用resolvereject时分别循环执行这两个数组里存储的回调函数。

面试官:可以,那promise的链式调用是怎么实现的呢?

比如:下面这段代码:

const p = new Promise((resolve, reject) => {setTimeout(() => {resolve(111)}, 1000)
})
p.then((value1) => {console.log('value1', value1)return 222
}, (error1) => {console.log('error1', error1)
}).then((value2) => {console.log('value2', value2)}, (error2) => {console.log('error2', error2)
})

它的打印结果为:

image.png

这个是如何实现的呢?

我:这个也简单,它内部调用then方法时,返回了一个新的promise,并让这个新的promise接管了它下一个then方法。

注意:这里不能返回this,这样会导致多个then方法全部受同一个promise控制。

class Promise {// ...then(onFulfilled, onRejected) {const promise2 = new Promise((resolve, reject) => {if (this.status === FULFILLED) {// onFulfilled方法可能返回值或者promiseconst x = onFulfilled(this.value)resolvePromise(promise2, x, resolve, reject)}if (this.status === REJECTED) {// onRejected方法可能返回值或者promiseconst x = onRejected(this.reason)resolvePromise(promise2, x, resolve, reject)}if (this.status === PENDING) {this.onResolvedCallbacks.push(() => {const x = onFulfilled(this.value)resolvePromise(promise2, x, resolve, reject)})this.onRejectedCallbacks.push(() => {const x = onRejected(this.reason)resolvePromise(promise2, x, resolve, reject)})}})return promise2}
}

最核心的就是resolvePromise,来看下它做了什么:

function resolvePromise(promise2, x, resolve, reject) {if (promise2 === x) {return reject(new TypeError('UnhandledPromiseRejectionWarning: TypeError: Chaining cycle detected for promise #<Promise>'))}let called// 判断x的类型 x是对象或函数才有可能是一个promiseif (typeof x === 'object' && x !== null || typeof x === 'function') {try {const then = x.thenif (typeof then === 'function') {// 只能认为它是一个promisethen.call(x, (y) => {if (called) returncalled = trueresolvePromise(promise2, y, resolve, reject)}, (r) => {if (called) returncalled = truereject(r)})}else {resolve(x)}} catch (e) {if (called) returncalled = truereject(e)}} else {resolve(x)}
}
  1. 首先,先判断新返回的一个promisepromise2是不是等于x,抛出错误UnhandledPromiseRejectionWarning: TypeError: Chaining cycle detected for promise #<Promise>,这一步是防止内部的循环引用。
  2. 声明一个变量called,相当于加了一把锁,让promise只能调用一次成功或者失败回调,防止死循环。
  3. 解析x,如果它的类型是object并且不为null,或者它是一个函数,并且它有then方法,我们认为这是一个promise
  4. 递归解析,then里面再次调用resolvePromise

手写最后

因为promiseEventLoop里面是个微任务,不过我们可以简单通过setTimout模拟。

然后我们再加上一些报错的捕获代码以及一些参数的兼容代码,以及实现catch方法。

class Promise {constructor(executor) {// ...// 这里增加try catchtry {executor(this.resolve, this.reject)} catch (e) {reject(e)}}then(onFulfilled, onRejected) {// 这里兼容下 onFulfilled 和 onRejected 的传参onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => vonRejected = typeof onRejected === 'function' ? onRejected : err => {throw err}const promise2 = new Promise((resolve, reject) => {if (this.status === FULFILLED) {// 用 setTimeout 模拟异步setTimeout(() => {try {const x = onFulfilled(this.value)resolvePromise(promise2, x, resolve, reject)} catch (e) {reject(e)}}, 0)}if (this.status === REJECTED) {// 用 setTimeout 模拟异步setTimeout(() => {try {const x = onRejected(this.reason)resolvePromise(promise2, x, resolve, reject)} catch (e) {reject(e)}}, 0)}if (this.status === PENDING) {this.onResolvedCallbacks.push(() => {// 用 setTimeout 模拟异步setTimeout(() => {try {const x = onFulfilled(this.value)resolvePromise(promise2, x, resolve, reject)} catch (e) {reject(e)}}, 0)})this.onRejectedCallbacks.push(() => {// 用 setTimeout 模拟异步setTimeout(() => {try {const x = onRejected(this.reason)resolvePromise(promise2, x, resolve, reject)} catch (e) {reject(e)}}, 0)})}})return promise2}// catch函数实际上里面就是调用了then方法catch (errCallback) {return this.then(null, errCallback)}
}
  1. executor执行时增加try catch,防止执行用户传入的函数直接就报错了,这时我们应该直接rejectpromise。
  2. 调用onFulfilledonRejected时,需要包裹setTimeout。 ok,这样就大功告成了。最后我们来测试下我们写的promise是否符合规范。
  3. catch函数实际上里面就是调用了then方法,然后第一个参数传null

测试promise

promise是有规范的,即Promises/A+,我们可以跑一段脚本测试写的promise是否符合规范。

首先,需要在我们的promise增加如下代码:

// 测试脚本
Promise.defer = Promise.deferred = function () {let dfd = {}dfd.promise = new Promise((resolve, reject) => {dfd.resolve = resolvedfd.reject = reject})return dfd
}

然后安装promises-aplus-tests包,比如用npm可以使用命令npm install -g promises-aplus-tests安装到全局,然后使用命令promises-aplus-tests 文件名即可进行测试,里面有872测试用例,全部通过即可以认为这是一个标准的promise

image.png

完美,最后面试官向你伸出了大拇指!

前端面试题库 (面试必备)            推荐:★★★★★

地址:前端面试题库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/50942.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RabbitMQ介绍

RabbitMQ的概念 RabbitMQ 是一个消息中间件&#xff1a;它接受并转发消息。你可以把它当做一个快递站点&#xff0c;当你要发送一个包裹时&#xff0c;你把你的包裹放到快递站&#xff0c;快递员最终会把你的快递送到收件人那里&#xff0c;按照这种逻辑 RabbitMQ 是 一个快递…

基于 Debian 12 的MX Linux 23 正式发布!

导读MX Linux 是基于 Debian 稳定分支的面向桌面的 Linux 发行&#xff0c;它是 antiX 及早先的 MEPIS Linux 社区合作的产物。它采用 Xfce 作为默认桌面环境&#xff0c;是一份中量级操作系统&#xff0c;并被设计为优雅而高效的桌面与如下特性的结合&#xff1a;配置简单、高…

微信开发之一键修改群聊备注的技术实现

修改群备注 修改群名备注后&#xff0c;如看到群备注未更改&#xff0c;是手机缓存问题&#xff0c;可以连续点击进入其他群&#xff0c;在点击进入修改的群&#xff0c;再返回即可看到修改后的群备注名&#xff0c;群名称的备注仅自己可见 请求URL&#xff1a; http://域名地…

ctfshow-红包题第二弹

0x00 前言 CTF 加解密合集CTF Web合集 0x01 题目 0x02 Write Up 同样&#xff0c;先看一下有没有注释的内容&#xff0c;可以看到有一个cmd的入参 执行之后可以看到文件代码&#xff0c;可以看到也是eval&#xff0c;但是中间对大部分的字符串都进行了过滤&#xff0c;留下了…

lvs实现DR模型搭建

目录 一&#xff0c;实现DR模型搭建 1&#xff0c; 负载调度器配置 1.1调整ARP参数 1.2 配置虚拟IP地址重启网卡 1.3 安装ipvsadm 1.4 加载ip_vs模块 1.5 启动ipvsadm服务 1.6 配置负载分配策略 1.7 保存策略 2&#xff0c; web节点配置 1.1 调整ARP参数 1.2 配置虚拟I…

Element Plus <el-table> 组件之展开行Table在项目中使用

目录 官方样式&#xff1a; 展开前&#xff1a; 展开&#xff1a; 原始代码&#xff1a; 代码详解&#xff1a; 项目使用场景&#xff1a; 完成效果&#xff1a; 具体实现范本&#xff1a; 1.调整数据结构 2. 修改标签和数据绑定 3. JavaScript 部分导入和创建对象 …

综合能源系统(8)——综合能源系统支撑技术

综合能源系统关键技术与典型案例  何泽家&#xff0c;李德智主编 1、大数据技术 1.1、大数据技术概述 大数据是指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合&#xff0c;是需要新处理模式才能具有更强的决策力、洞察发现力和流程优化能力的海量、高…

wps设置其中几页为横版

问题&#xff1a;写文档的时候&#xff0c;有些表格列数太多&#xff0c;页面纵向显示内容不完整&#xff0c;可以给它改成横向显示。 将鼠标放在表格上一页的底部&#xff0c;点击‘插入-分页-下一页分节符’。 将鼠标放在表格页面的底部&#xff0c;点击‘插入-分页-下一页分…

【Docker入门第一篇】

Docker简介 Docker 是一个开源的应用容器引擎&#xff0c;基于 Go 语言 并遵从 Apache2.0 协议开源。 Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中&#xff0c;然后发布到任何流行的 Linux 机器上&#xff0c;也可以实现虚拟化。 容器是完全使…

《数字图像处理-OpenCV/Python》连载(2)目录

《数字图像处理-OpenCV/Python》连载&#xff08;2&#xff09;目录 本书京东优惠购书链接&#xff1a;https://item.jd.com/14098452.html 本书CSDN独家连载专栏&#xff1a;https://blog.csdn.net/youcans/category_12418787.html 第一部分 OpenCV-Python的基本操作 第1章 …

Redis多机实现

Background 为啥要有多机--------------1.容错 2.从服务器分担读压力。 主从结构一大难题------------如何保障一致性&#xff0c;对这个一致性要求不是很高&#xff0c;因为redis是用来做缓存的 同时我们要自动化进行故障转移-------哨兵机制&#xff0c;同时哨兵也可能cra…

江西南昌电气机械三维测量仪机械零件3d扫描-CASAIM中科广电

精密机械零部件是指机械设备中起到特定功能的零件&#xff0c;其制造精度要求非常高。这些零部件通常由金属、塑料或陶瓷等材料制成&#xff0c;常见的精密机械零部件包括齿轮、轴承、螺丝、活塞、阀门等。精密机械零部件的制造需要高精度的加工设备和工艺&#xff0c;以确保其…

HJ31 单词倒排 题解

题目描述&#xff1a;单词倒排_牛客题霸_牛客网 (nowcoder.com) 对字符串中的所有单词进行倒排。 1、构成单词的字符只有26个大写或小写英文字母&#xff1b; 2、非构成单词的字符均视为单词间隔符&#xff1b; 3、要求倒排后的单词间隔符以一个空格表示&#xff1b;如果原字符…

opencv 车牌的定位与分割+UI界面

目录 一、实现和完整UI视频效果展示 主界面&#xff1a; 识别结果界面&#xff1a; 查看分割处理过程图片界面&#xff1a; 二、原理介绍&#xff1a; 加权灰度化 ​编辑 二值化 滤波降噪处理 锐化处理 边缘特征提取 图像分割 完整演示视频&#xff1a; 代码链接 …

musl libc ldso 动态加载研究笔记:动态库的搜索路径

前言 在手动设置动态库的存放路径的情况下&#xff0c;发现 musl ldso 依旧可以加载位于 /lib 目录下的 动态共享库 动态库的存放路径或者说搜索路径&#xff0c;是否可以手动配置&#xff1f;比如 Linux 上 有个配置文件可以配置&#xff0c;可以改变 动态库的搜索次序&#…

【1-3章】Spark编程基础(Python版)

课程资源&#xff1a;&#xff08;林子雨&#xff09;Spark编程基础(Python版)_哔哩哔哩_bilibili 第1章 大数据技术概述&#xff08;8节&#xff09; 第三次信息化浪潮&#xff1a;以物联网、云计算、大数据为标志 &#xff08;一&#xff09;大数据 大数据时代到来的原因…

Unity之 Vector3 的详细介绍以及方法的介绍

文章目录 总的介绍小试牛刀相关的描述的参数看个小例子 总的介绍 当涉及到Unity中的Vector3类时&#xff0c;以下是一些常用的方法和操作&#xff1a; magnitude 方法&#xff1a;返回向量的长度。 float length vector.magnitude;sqrMagnitude 方法&#xff1a;返回向量的平…

恒运资本:信创概念再度活跃,华是科技再创新高,南天信息等涨停

信创概念21日盘中再度活跃&#xff0c;截至发稿&#xff0c;华是科技涨超17%&#xff0c;盘中一度触及涨停再创新高&#xff0c;中亦科技涨超13%亦创出新高&#xff0c;久其软件、南天信息、新炬网络、英飞拓均涨停。 音讯面上&#xff0c;自8月3日以来&#xff0c;财政部官网连…

Dockerfile制作镜像与搭建LAMP环境

1、编写Dockerfile制作Web应用系统nginx镜像&#xff0c;生成镜像nginx:v1.1&#xff0c;并推送其到私有仓库。 具体要求如下&#xff1a; &#xff08;1&#xff09;基于centos基础镜像&#xff1b; &#xff08;2&#xff09;指定作者信息&#xff1b; &#xff08;3&#x…

Window Server 与 Windows 系统开关机日志查看方法

目录 Windows/Windows Server 查看日志Windows 系统常用的事件 ID 环境&#xff1a;Windows Server 2019 &#xff08;也适用于 Windows 其他系统&#xff09;。 不同版本的 Windows 图标可能有所不同&#xff0c;但是服务器级 Windows Server 与普通桌面级 Windows 还会有些操…