百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

图解 Promise 实现原理(一):基础实现

liuian 2025-08-01 18:42 74 浏览


作者:孔垂亮

转发链接:
https://mp.weixin.qq.com/s/UNzYgpnKzmW6bAapYxnXRQ

前言

很多同学在学习 Promise 时,知其然却不知其所以然,对其中的用法理解不了。本系列文章由浅入深逐步实现 Promise,并结合流程图、实例以及动画进行演示,达到深刻理解 Promise 用法的目的。

本系列文章由如下几个章节组成:

  1. 图解 Promise 实现原理(一)—— 基础实现 (本篇)
  2. 图解 Promise 实现原理(二):Promise 链式调用
  3. 图解 Promise 实现原理(三):Promise 原型方法实现
  4. 图解 Promise 实现原理(四):Promise 静态方法实现

本文适合对 Promise 的用法有所了解的人阅读,如果还不清楚,请自行查阅阮一峰老师的 《ES6入门 之 Promise 对象》。

Promise 规范有很多,如 Promise/A,Promise/B,Promise/D 以及 Promise/A 的升级版 Promise/A+,有兴趣的可以去了解下,最终 ES6 中采用了 Promise/A+ 规范。所以本文的Promise源码是按照Promise/A+规范来编写的(不想看英文版的移步Promise/A+规范中文翻译)。

【案例】

为了让大家更容易理解,我们从一个场景开始,一步一步跟着思路思考,会更容易看懂。

考虑下面一种获取用户 id 的请求处理:

//不使用Promise        
http.get('some_url', function (result) {
    //do something
    console.log(result.id);
});

//使用Promise
new Promise(function (resolve) {
    //异步请求
    http.get('some_url', function (result) {
        resolve(result.id)
    })
}).then(function (id) {
    //do something
    console.log(id);
})

(滑动可查看)

乍一看,好像不使用 Promise 更简洁一些。其实不然,设想一下,如果有好几个依赖的前置请求都是异步的,此时如果没有 Promise ,那回调函数要一层一层嵌套,看起来就很不舒服了。如下:

//不使用Promise        
http.get('some_url', function (id) {
    //do something
    http.get('getNameById', id, function (name) {
        //do something
        http.get('getCourseByName', name, function (course) {
            //dong something
            http.get('getCourseDetailByCourse', function (courseDetail) {
                //do something
            })
        })
    })
});

//使用Promise
function getUserId(url) {
    return new Promise(function (resolve) {
        //异步请求
        http.get(url, function (id) {
            resolve(id)
        })
    })
}
getUserId('some_url').then(function (id) {
    //do something
    return getNameById(id); // getNameById 是和 getUserId 一样的Promise封装。下同
}).then(function (name) {
    //do something
    return getCourseByName(name);
}).then(function (course) {
    //do something
    return getCourseDetailByCourse(course);
}).then(function (courseDetail) {
    //do something
});

(滑动可查看)

【实现原理】

说到底,Promise 也还是使用回调函数,只不过是把回调封装在了内部,使用上一直通过 then 方法的链式调用,使得多层的回调嵌套看起来变成了同一层的,书写上以及理解上会更直观和简洁一些。

一、基础版本

//极简的实现
class Promise {
    callbacks = [];
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        this.callbacks.push(onFulfilled);
    }
    _resolve(value) {
        this.callbacks.forEach(fn => fn(value));
    }
}

//Promise应用
let p = new Promise(resolve => {
    setTimeout(() => {
        console.log('done');
        resolve('5秒');
    }, 5000);
}).then((tip) => {
    console.log(tip);
})

(滑动可查看)

上述代码很简单,大致的逻辑是这样的:

  1. 调用 then 方法,将想要在 Promise 异步操作成功时执行的 onFulfilled 放入callbacks队列,其实也就是注册回调函数,可以向观察者模式方向思考;
  2. 创建 Promise 实例时传入的函数会被赋予一个函数类型的参数,即 resolve,它接收一个参数 value,代表异步操作返回的结果,当异步操作执行成功后,会调用resolve方法,这时候其实真正执行的操作是将 callbacks 队列中的回调一一执行。



(图:基础版本实现原理)

首先 new Promise 时,传给 Promise 的函数设置定时器模拟异步的场景,接着调用 Promise 对象的 then 方法注册异步操作完成后的 onFulfilled,最后当异步操作完成时,调用 resolve(value), 执行 then 方法注册的 onFulfilled。

then 方法注册的 onFulfilled 是存在一个数组中,可见 then 方法可以调用多次,注册的多个onFulfilled 会在异步操作完成后根据添加的顺序依次执行。如下:

//then 的说明
let p = new Promise(resolve => {
    setTimeout(() => {
        console.log('done');
        resolve('5秒');
    }, 5000);
});

p.then(tip => {
    console.log('then1', tip);
});

p.then(tip => {
    console.log('then2', tip);
});

(滑动可查看)

上例中,要先定义一个变量 p ,然后 p.then 两次。而规范中要求,then 方法应该能够链式调用。实现也简单,只需要在 then 中 return this 即可。如下所示:

//极简的实现+链式调用
class Promise {
    callbacks = [];
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        this.callbacks.push(onFulfilled);
        return this;//看这里
    }
    _resolve(value) {
        this.callbacks.forEach(fn => fn(value));
    }
}

let p = new Promise(resolve => {
    setTimeout(() => {
        console.log('done');
        resolve('5秒');
    }, 5000);
}).then(tip => {
    console.log('then1', tip);
}).then(tip => {
    console.log('then2', tip);
});

(滑动可查看)



(图:基础版本的链式调用)

二、加入延迟机制

上面 Promise 的实现存在一个问题:如果在 then 方法注册 onFulfilled 之前,resolve 就执行了,onFulfilled 就不会执行到了。比如上面的例子中我们把 setTimout 去掉:

//同步执行了resolve
let p = new Promise(resolve => {
    console.log('同步执行');
    resolve('同步执行');
}).then(tip => {
    console.log('then1', tip);
}).then(tip => {
    console.log('then2', tip);
});

(滑动可查看)

执行结果显示,只有 "同步执行" 被打印了出来,后面的 "then1" 和 "then2" 均没有打印出来。再回去看下 Promise 的源码,也很好理解,resolve 执行时,callbacks 还是空数组,还没有onFulfilled 注册上来。

这显然是不允许的,Promises/A+规范明确要求回调需要通过异步方式执行,用以保证一致可靠的执行顺序。因此要加入一些处理,保证在 resolve 执行之前,then 方法已经注册完所有的回调:

//极简的实现+链式调用+延迟机制
class Promise {
    callbacks = [];
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        this.callbacks.push(onFulfilled);
        return this;
    }
    _resolve(value) {
        setTimeout(() => {//看这里
            this.callbacks.forEach(fn => fn(value));
        });
    }
}

(滑动可查看)

在 resolve 中增加定时器,通过 setTimeout 机制,将 resolve 中执行回调的逻辑放置到JS任务队列末尾,以保证在 resolve 执行时,then方法的 onFulfilled 已经注册完成。


(图:延迟机制)

但是这样依然存在问题,在 resolve 执行后,再通过 then 注册上来的 onFulfilled 都没有机会执行了。如下所示,我们加了延迟后,then1 和 then2 可以打印出来了,但下列中的 then3 依然打印不出来。所以我们需要增加状态,并且保存 resolve 的值。

let p = new Promise(resolve => {
    console.log('同步执行');
    resolve('同步执行');
}).then(tip => {
    console.log('then1', tip);
}).then(tip => {
    console.log('then2', tip);
});

setTimeout(() => {
    p.then(tip => {
        console.log('then3', tip);
    })
});

(滑动可查看)

三、增加状态

为了解决上一节抛出的问题,我们必须加入状态机制,也就是大家熟知的 pending、fulfilled、rejected。

Promises/A+ 规范中明确规定了,pending 可以转化为 fulfilled 或 rejected 并且只能转化一次,也就是说如果 pending 转化到 fulfilled 状态,那么就不能再转化到 rejected。并且 fulfilled 和 rejected 状态只能由 pending 转化而来,两者之间不能互相转换。



增加状态后的实现是这样的

//极简的实现+链式调用+延迟机制+状态
class Promise {
    callbacks = [];
    state = 'pending';//增加状态
    value = null;//保存结果
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        if (this.state === 'pending') {//在resolve之前,跟之前逻辑一样,添加到callbacks中
            this.callbacks.push(onFulfilled);
        } else {//在resolve之后,直接执行回调,返回结果了
            onFulfilled(this.value);
        }
        return this;
    }
    _resolve(value) {
        this.state = 'fulfilled';//改变状态
        this.value = value;//保存结果
        this.callbacks.forEach(fn => fn(value));
    }
}

(滑动可查看)

注意:当增加完状态之后,原先的_resolve中的定时器可以去掉了。当reolve同步执行时,虽然callbacks为空,回调函数还没有注册上来,但没有关系,因为后面注册上来时,判断状态为fulfilled,会立即执行回调。


(图:Promise 状态管理)

实现源码中只增加了 fulfilled 的状态 和 onFulfilled 的回调,但为了完整性,在示意图中增加了 rejected 和 onRejected 。后面章节会实现。

resolve 执行时,会将状态设置为 fulfilled ,并把 value 的值存起来,在此之后调用 then 添加的新回调,都会立即执行,直接返回保存的value值。

(Promise 状态变化演示动画)

至此,一个初具功能的Promise就实现好了,它实现了 then,实现了链式调用,实现了状态管理等等。但仔细想想,链式调用的实现只是在 then 中 return 了 this,因为是同一个实例,调用再多次 then 也只能返回相同的一个结果,这显然是不能满足我们的要求的。下一节,讲述如何实现真正的链式调用。

推荐JavaScript经典实例学习资料文章

实践教你从零构建前端 Lint 工作流「干货」

高性能多级多选级联组件开发「JS篇」

深入浅出讲解Node.js CLI 工具最佳实战

延迟加载图像以提高Web网站性能的五种方法「实践」

比较 JavaScript 对象的四种方式「实践」

使用Service Worker让你的 Web 应用如虎添翼(上)「干货」

使用Service Worker让你的 Web 应用如虎添翼(中)「干货」

使用Service Worker让你的 Web 应用如虎添翼(下)「干货」

前端如何一次性处理10万条数据「进阶篇」

推荐三款正则可视化工具「JS篇」

如何让用户选择是否离开当前页面?「JS篇」

JavaScript开发人员更喜欢Deno的五大原因

仅用18行JavaScript实现一个倒数计时器

图文细说JavaScript 的运行机制

一个轻量级 JavaScript 全文搜索库,轻松实现站内离线搜索

推荐Web程序员常用的15个源代码编辑器

10个实用的JS技巧「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(一)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(二)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(三)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(四)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(五)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(六)「值得收藏」

深入JavaScript教你内存泄漏如何防范

手把手教你7个有趣的JavaScript 项目-上「附源码」

手把手教你7个有趣的JavaScript 项目-下「附源码」

JavaScript 使用 mediaDevices API 访问摄像头自拍

手把手教你前端代码如何做错误上报「JS篇」

一文让你彻底搞懂移动前端和Web 前端区别在哪里

63个JavaScript 正则大礼包「值得收藏」

提高你的 JavaScript 技能10 个问答题

JavaScript图表库的5个首选

一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法

可视化的 JS:动态图演示 - 事件循环 Event Loop的过程

教你如何用动态规划和贪心算法实现前端瀑布流布局「实践」

可视化的 js:动态图演示 Promises & Async/Await 的过程

原生JS封装拖动验证滑块你会吗?「实践」

如何实现高性能的在线 PDF 预览

细说使用字体库加密数据-仿58同城

Node.js要完了吗?

Pug 3.0.0正式发布,不再支持 Node.js 6/8

纯JS手写轮播图(代码逻辑清晰,通俗易懂)

JavaScript 20 年 中文版之创立标准

值得收藏的前端常用60余种工具方法「JS篇」

箭头函数和常规函数之间的 5 个区别

通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events

「前端篇」不再为正则烦恼

「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能

深入细品浏览器原理「流程图」

JavaScript 已进入第三个时代,未来将何去何从?

前端上传前预览文件 image、text、json、video、audio「实践」

深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系

推荐13个有用的JavaScript数组技巧「值得收藏」

前端必备基础知识:window.location 详解

不要再依赖CommonJS了

犀牛书作者:最该忘记的JavaScript特性

36个工作中常用的JavaScript函数片段「值得收藏」

Node + H5 实现大文件分片上传、断点续传

一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」

【实践总结】关于小程序挣脱枷锁实现批量上传

手把手教你前端的各种文件上传攻略和大文件断点续传

字节跳动面试官:请你实现一个大文件上传和断点续传

谈谈前端关于文件上传下载那些事【实践】

手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件

最全的 JavaScript 模块化方案和工具

「前端进阶」JS中的内存管理

JavaScript正则深入以及10个非常有意思的正则实战

前端面试者经常忽视的一道JavaScript 面试题

一行JS代码实现一个简单的模板字符串替换「实践」

JS代码是如何被压缩的「前端高级进阶」

前端开发规范:命名规范、html规范、css规范、js规范

【规范篇】前端团队代码规范最佳实践

100个原生JavaScript代码片段知识点详细汇总【实践】

关于前端174道 JavaScript知识点汇总(一)

关于前端174道 JavaScript知识点汇总(二)

关于前端174道 JavaScript知识点汇总(三)

几个非常有意思的javascript知识点总结【实践】

都2020年了,你还不会JavaScript 装饰器?

JavaScript实现图片合成下载

70个JavaScript知识点详细总结(上)【实践】

70个JavaScript知识点详细总结(下)【实践】

开源了一个 JavaScript 版敏感词过滤库

送你 43 道 JavaScript 面试题

3个很棒的小众JavaScript库,你值得拥有

手把手教你深入巩固JavaScript知识体系【思维导图】

推荐7个很棒的JavaScript产品步骤引导库

Echa哥教你彻底弄懂 JavaScript 执行机制

一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧

深入解析高频项目中运用到的知识点汇总【JS篇】

JavaScript 工具函数大全【新】

从JavaScript中看设计模式(总结)

身份证号码的正则表达式及验证详解(JavaScript,Regex)

浏览器中实现JavaScript计时器的4种创新方式

Three.js 动效方案

手把手教你常用的59个JS类方法

127个常用的JS代码片段,每段代码花30秒就能看懂-【上】

深入浅出讲解 js 深拷贝 vs 浅拷贝

手把手教你JS开发H5游戏【消灭星星】

深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】

手把手教你全方位解读JS中this真正含义【实践】

书到用时方恨少,一大波JS开发工具函数来了

干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)

手把手教你JS 异步编程六种方案【实践】

让你减少加班的15条高效JS技巧知识点汇总【实践】

手把手教你JS开发H5游戏【黄金矿工】

手把手教你JS实现监控浏览器上下左右滚动

JS 经典实例知识点整理汇总【实践】

2.6万字JS干货分享,带你领略前端魅力【基础篇】

2.6万字JS干货分享,带你领略前端魅力【实践篇】

简单几步让你的 JS 写得更漂亮

恭喜你获得治疗JS this的详细药方

谈谈前端关于文件上传下载那些事【实践】

面试中教你绕过关于 JavaScript 作用域的 5 个坑

Jquery插件(常用的插件库)

【JS】如何防止重复发送ajax请求

JavaScript+Canvas实现自定义画板

Continuation 在 JS 中的应用「前端篇」


作者:孔垂亮

转发链接:
https://mp.weixin.qq.com/s/UNzYgpnKzmW6bAapYxnXRQ

相关推荐

戴尔笔记本电脑一开机就蓝屏

笔记本蓝屏可能是电脑硬盘故障,可以更换一个硬盘尝试。也可能是更新了驱动与修复漏洞补丁,可以进入安全模式将更新的驱动删除。有可能是内存条故障,可以把内存条取下来,用橡皮擦轻轻擦拭金手指,然后用毛刷将内存...

优酷路由宝怎么设置(优酷路由宝怎么设置网络)

无线连接如果准备用手机、笔记本电脑来设置优酷路由宝,需要先把WAN口,连接宽带网线(宽带猫、光猫);然后手机/笔记本电脑搜索连接到优酷路由宝的WiFi。优酷路由宝的默认WiFi名称是:Youku_开...

一键装机软件大全(一键装机下载)

1一键装机工具是一种自动化安装计算机操作系统以及常用软件的工具。2使用一键装机工具,需要先准备好需要安装的操作系统镜像和需要安装的软件列表,然后将它们放在一键装机工具所指定的位置。接下来,打开一键...

home键是什么意思苹果手机(home键是苹果手机哪个键)

就是手机屏幕正下方的那个圆形的按钮,就是苹果手机的home键,home键的作用比较大,可以用来设置指纹解锁,单机home键可以返回主屏幕界面,双击home键可以弹出后台应用程序可以进行清楚,还可以通过...

tplink说明书图片(tp-link路由器说明书步骤图)

第一步连接路由器WIFI在手机获取IP地址里找到路由器网关地址,第二步在浏览器地址栏输入路由器网关地址,之后会跳转到路由器管理员登录界面,输入账号密码就可以进入路由后台管理路由,如果提示路由器密码错误...

如何不安装flash玩4399(现在4399不提供flash如何玩游戏)

没有flash是玩不了的,需要开启flash才可以。1、首先打开浏览器,进入4399的游戏页面。2、进入游戏页面后,点击【已被屏蔽】文字。3、然后右上角会出现窗口,点击【管理】按钮。4、进入管理页面后...

chrome download apk(chromedownloadapk in english)

手机下载安装的第三方应用出现问题,无法正常使用,建议按照以下方法操作:1.关闭重新启动该应用。2.建议将此软件卸载重新安装尝试。3.更换其他版本尝试。4.更新下手机系统版本后安装尝试5.备份手机数据(...

qq空间官网手机登录网页版(qq空间官网登陆入口)
qq空间官网手机登录网页版(qq空间官网登陆入口)

z.qq.com可以通过以下方式登录手机QQ空间:1、使用手机登录手机腾讯网3g.qq.com,点击“空间”,根据提示QQ号码和QQ密码就可以登录;2、通过手机直接输入手机QQ空间网址z.qq.com,根据提示操作即可登录;3、下载手机Q...

2025-12-22 13:55 liuian

windows11我的电脑在哪里打开

1/6通过“开始”进入“设置”-“时间和语言”。2/6在“时间和语言”界面选择“区域”3/6这里我们将区域更改位“新加披”,退出。4/6打开微软自带的市场,搜索“你的手机”获取并下载。5/6安装完成后...

win10怎么取消开机自启动(win10如何关闭开机自动启动)

要关闭Windows10的开机自动启动程序,你可以按下Win+R键,输入"msconfig"并按回车键打开系统配置工具。在"启动"选项卡中,你可以看到所有开机自动...

手机cpu排名2025(手机cpu排名榜)

一、2022手机CPU性能综合排名前八名手机CPU:1、型号:苹果A16---综合分数:暂无2、型号:骁龙8gen1---综合分数:42333、联发科天玑9000---综合分数:38724、...

论坛系统(论坛系统数据流图)

BBS是电子布告栏系统的简称,一种网站系统,也是目前流行网络论坛的前身。它允许用户使用终端程序通过调制解调器拨接或者因特网来进行连接,BBS站台提供布告栏、分类讨论区、新闻阅读、软件下载与上传、游戏、...

hp1020plus打印机无法打印(惠普1020plus打印机突然不能打印了)

 删除惠普打印机驱动和软件:1.如果你的打印机已通过USB连接到电脑,断开USB连接;2.打开控制面板—程序和功能(卸载或更改应用程序);3.在软件列表中找到惠普打印机,将其卸载;4.重启电脑...

wifi密码破解器电脑版(wifi密码破解工具电脑版)

肯定不是万能钥匙这种“破解”wifi的东西。不是一两次见到把万能钥匙当做破解wifi用的人了,但实际上那玩意就是个分享wifi的软件。你连上一个wifi,密码就会被分享到云端(可以不分享),别...

手机临时文件夹在哪个位置(手机临时文件夹在哪个位置找)

1.手机文件临时文件是指在手机使用过程中产生的临时文件。2.手机应用程序在运行时需要产生一些临时文件,如缓存文件、日志文件、临时下载文件等,这些文件可以提高应用程序的运行效率和用户体验。但是,这些...