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

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

myzbx 2025-05-09 20:33 3 浏览


作者:孔垂亮

转发链接:
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

相关推荐

MORROR ART:毫无音质可言,真的只是好看而已...

今天早上我在微博上发了一条短视频,内容是某款网红音箱正在放声歌唱——这玩意就是此前曾经在网上挺火的所谓“悬浮歌词音箱”。这款产品是我同事收到的礼品,但她嫌在家里放着没用,所以拿到公司里做我们的拍摄道具...

「JS优化篇」你的 if - else 代码肯定没我写的好

作者:小生方勤转发链接:https://mp.weixin.qq.com/s/JzOQ_OwAYoP5Ic1VBtCZNA前言最近部门在对以往的代码做一些优化,我在代码中看到一连串的if(){}el...

细聊微内核架构在前端的应用「干货」

作者:semlinker转发链接:https://mp.weixin.qq.com/s/ywc98dS4TVB4t3L2tIyk8g一、微内核架构简介1.1微内核的概念微内核架构(Microke...

ThreeJS 入门教程(一) 是选择桌面的固守还是云原生?

导读:最近我购置了一台新的电脑,硬盘空间只有1T。我很担心这个电脑还能用多久。性能限制或者空间的限制,都使得在未来3-5年内,这个电脑会被淘汰。但是,基于云APP的使用,老的电脑是足够了,而且,我们也...

推荐三款正则可视化工具「JS篇」(正则在线调试)

作者:代码先森转发链接:https://mp.weixin.qq.com/s/rw29yKBwti5sIsx2GKG9pw前言最近老王对可视化非常着迷。例如,算法可视化、正则可视化、Vue数据劫持可...

Javascript 多线程编程的前世今生

作者:jolamjiang腾讯技术工程转发链接:https://mp.weixin.qq.com/s/87C9GAFb0Y_i5iPbIL5Hzg为什么要多线程编程大家看到文章的标题《Javasc...

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

作者:李俊辰前端之巅转发链接:https://mp.weixin.qq.com/s/q-49Gf-SFijeu7d2MqztIQ前言近日,Pug3.0.0正式发布,Pug原名Jade,是由...

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

作者:Eno_Yao转发链接:https://segmentfault.com/a/1190000022623676前言如果文章和笔记能带您一丝帮助或者启发,请不要吝啬你的赞和收藏,你的肯定是我前进的...

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

作者:大道至简转发链接:https://mp.weixin.qq.com/s/0w6aWwpR3MAJnmyLwDnAzA前言一般情况下,忽视内存管理不会对传统的网页产生显著的后果。这是因为,用户刷新...

由浅入深,66条JavaScript面试知识点(七)

作者:JakeZhang转发链接:https://juejin.im/post/5ef8377f6fb9a07e693a6061目录由浅入深,66条JavaScript面试知识点(一)由浅入深,66...

用STM32做了个电子秤,成本仅两位数,精度高!解析一下原理

俗话说得好!人在胖,秤在看!所以,我想DIY一个精度高的体重秤!并希望它不只能称体重:还能像这样称克重(可设置KG,G,最低可称100克)……这样一来,做甜品的时候,还能拿来应应急。保姆级教程,记录在...

前端开发需要了解常用7种JavaScript设计模式

作者|Deven译者|王强策划|小智转发链接:https://mp.weixin.qq.com/s/Lw4D7bfUSw_kPoJMD6W8gg前言JavaScript中的设计模式指的是...

毛姆的一个手法|王培军(毛姆作品简介)

鲁本斯画《海伦娜·芙尔曼肖像》钱锺书在《宋诗选注》文同小传中说:“具体的把当前风物比拟为某种画法或某某大画家的名作”,是“从文同正式起头”。如钱先生所举的:“峰峦李成似,涧谷范宽能”,“独坐水轩人不到...

欣赏 | 朝戈:我渴望找到直达心灵的永恒

朋友,通过艺术让我们共同感知世界的永恒与不朽。——朝戈橙色的人物117X71cm布面油画2003包与陈185cm×103cm2007年白色80cm×40cm2009年光布面油画-Light-Oilo...

Web页面如此耗电!到了某种程度,会是大损失

现在用户上网大多使用移动设备或者笔记本电脑。对这两者来说,电池寿命都很重要。在这篇文章里,我们将讨论影响电池寿命的因素,以及作为一个web开发者,我们如何让网页耗电更少,以便用户有更多时间来关注我们的...