JS Promise的实现原理

转载自   JS Promise的实现原理

 在前端开发过程中,会经常使用到 Promise 模式,可以使异步代码看起来如同步般清新易读,从而从回调地狱中解脱出来。ES6中 已原生支持 Promise,但在未支持的浏览器中还需要通过 polyfill 模拟实现。下面介绍一下自己的实现过程,此实现可通过 Promise/A+测试集 的所有测试。

  Promise 是一个关联了执行任务的承诺,当你的任务完成时,会根据任务的成功与否,执行相应的操作。所以创建 promise 对象时,构造函数中需要传递一个函数类型的参数(Chrome 的实现中,参数名叫resolver,我觉得叫taskworker也许会直观一些。但此处采用它的命名,谁叫我是 Chrome 粉呢),来作为与此 promise 对象关联的任务。因此,现在 Promise 构造函数定义如下:

function Promise(resolver) {}

  Promise 对象有三种状态:pendingfullfilled 和 rejected,分别代表了 promise 对象处于等待、执行成功和执行失败状态。创建 promise 对象后处于pending状态,pending状态可以转化为fullfilledrejected状态,但不能逆向转化,且转化过程只能有一次,即resolvereject后不能再resolvereject。因此需要在 promise 对象中持有状态的引用,通过添加一个名为_state(为了说明是内部属性,用户不要直接使用,属性名前加了下划线,后面同理)的属性实现。现在 Promise 构造函数定义如下:

function Promise(resolver) {this._status = 'pending';
}

  任务(resolver)内封装了需要执行的异步操作(当然,也可以是同步操作)。同时resolver调用时会被传递两个参数:resolvereject函数,来自于 Promise 内部的封装,分别代表任务执行成功或者失败时需要执行的操作。任务成功与否由调用者控制,且需要在成功或失败时调用resolvereject函数,以此来标识当前 promise 对象的完成,并会触发后续 promise 的执行。

  在调用Promise构造函数时,resolver会被立即调用。因此,现在Promise构造函数如下:

function Promise(resolver) {this._status = 'pending';resolver(resolve, reject);...
}

  Promise 代表着一个承诺。作为承诺,总需要有一个结果,无论成功与否。如果成功,我们会获得需要的结果;当然也有可能会失败。因此我们需要在这个承诺在未来某个时刻有结果时,分别针对结果的成功或失败做相应的处理。因此 Promise 中提供了then方法来完成这个任务。then方法接收两个参数:onResolveonReject,分别代表当前 promise 对象在成功或失败时,接下来需要做的操作。现实生活中,人们总系喜欢给出各种许诺,同样在代码的世界里,我们也经常会有一连串前后依赖的 promise 需要执行,如下面的调用方式:promise.then().then()...。因此为了方便链式调用,then方法的实现中,都会返回一个新的 promise 对象,就像 jQuery 的方法中一般都会将自己(this)返回一样(不同的是,jQuery中返回的是自身,但在 Promise 中,返回的是一个新的 promise 对象。如果此处也返回自身的话,则串行操作就变成并行操作了,显然不符合我们的目标)。因此,then方法的定义如下:

Promise.prototype.then = function(onResolve, onReject) {var promise = new Promise(function() {});...return promise;
}

  此处then方法内创建的 promise 对象和暴露给用户直接调用的 Promise 构造函数所创建的 promise 对象有些不同。用户调用 Promise 构造函数时需要传递resolver参数代表与此 promise 对象关联的任务,且任务会立即执行。在未来某个时刻,用户根据任务执行的结果来判断任务是成功还是失败,并且需要调用resolver中被传入的参数resolvereject来结束此 promise,并由此触发下一个 promise(即当前 promise 对象调用then方法所创建的 promise 对象)所关联的任务的执行。由此可知以下两点:首先then方法中创建的 promise 关联的任务不能在 promise 对象创建时立即执行,所以先传入一个空函数以符合 Promise 构造函数调用格式;其次前一个 promise 对象需要能够知道下一个 promise 对象是谁,其关联的任务是什么,这样才能在自己完成后调用下一个 promise 的任务。因此前一个 promise 需要持有下一个 promise 以及其任务的引用。由于 promise 的执行可能会成功也可能会失败,因此后一个 promise 一般会提供成功或失败后需要执行的任务供前一个 promise 调用。因此前一个 promise 持有下一个 promise 的任务引用时需要区分这一点。promise 的调用不一定都如promise.then().then()...这样的串行方式,也可以有如下的并行方式:

    var promise = new Promise(xxx);promise.then();promise.then();...

  此时当前一个 promise 对象完成后,会同时调用两个then方法中创建的 promise 关联的任务。因此,前一个 promise 对象可能需要持有多个 promise 对象以及它们关联的成功和失败任务的引用。因此需要给 promise 对象添加属性用于这些数据的记录。可以有不同的方式实现,如可以添加一个对象数组属性,数组中的每一项是一个对象,里面有下一个 promise 以及成功、失败回调的引用。即如下:

 [{promise: promise1,doneCallback: doneCallback1,failCallback: failCallback1},{promise: promise2,doneCallback: doneCallback2,failCallback: failCallback2},...]

  当然也可以有其它的方式实现。此处我采用了闭包的方式实现:在 promise 对象中增加分别代表成功回调和失败回调的两个数组,数组中的每一项是通过内部封装的闭包函数调用的结果,也是一个函数。只不过这个函数可以访问到内部调用闭包时传递的 promise 对象,因此通过这种方式也可以访问到我们需要的下一个 promise 以及其关联的成功、失败回调的引用。所以现在有两处改动。首先需要在 Promise 构造函数中增加两个属性。现在 Promise 构造函数的定义如下:

function Promise(resolver) {this._status = 'pending';this._doneCallbacks = [];this._failCallbacks = [];resolver(resolve, reject);...
}

  其次,需要在then方法中增加闭包调用以及为前一个 promise 对象保存引用。现在then的定义如下:

Promise.prototype.then = function(onResolve, onReject) {var promise = new Promise(function() {});this._doneCallbacks.push(makeCallback(promise, onResolve, 'resolve'));this._failCallbacks.push(makeCallback(promise, onReject, 'reject'));return promise;
}

  then方法中调用的makeCallback即上面说到的闭包函数。调用时会把 promise 对象以及相应的回调传递进去,且会返回一个新的函数,前一个 promise 对象持有返回函数的引用,这样在调用返回函数时,在函数内部就可以访问到 promise 对象以及回调函数了。由于成功回调onResolve和失败回调onReject都通过此闭包封装,所以在闭包中增加了第三个参数action,以区分是哪种回调。现在makeCallback的定义如下:

function makeCallback(promise, callback, action) {return function promiseCallback(value) {...};
}

  前面说过,调用构造函数创建 promise 对象时需要传递作为任务的函数resolverresolver会被立即调用,并被传递参数resolvereject函数,用于结束当前 promise 并触发接下来的 promise 的调用。下面将介绍resolvereject函数的实现。

  我们使用 promise,是期望在未来的某个时刻能获得一个结果,并且可用于接下来的 promise 调用。所以resolve函数需要有一个参数来接收结果(同样,promise 执行失败后,我们也希望在后续 promise 中获得此失败信息,做相应处理。所以reject函数也需要有一个参数来接收错误)。前面说过 promise 对象的状态只能由pending状态转换为fullfilledrejected状态,且只能转换一次。所以resolvereject时,需要判断一下状态。所以,现在resolvereject函数的定义如下:

 function resolve(promise, data) {if (promise._status !== 'pending') {return;}promise._status = 'fullfilled';promise._value = data;run(promise);}function reject(promise, reason) {if (promise._status !== 'pending') {return;}promise._status = 'rejected';promise._value = reason;run(promise);}

  resolvereject函数也可以定义在 Promise 构造函数的 prototype 上,这样可直接通过promise.resolve(data)promise.reject(reason)调用,不用传递第一个参数promise。但由于此函数是内部调用,为了不暴露不必要的接口给用户,所以定义为内部函数。由于执行时需要知道是 resolve 或 reject 哪一个 promise 对象,所以需要多一个名为promise参数。resolvereject函数中首先判断了当前 promise 的状态,如果不是pending(即已经被 resolve 或 reject 过了,不再重复执行),则直接返回。然后赋予 promise 新的状态,并保存成功或失败的值。最后调用run函数。run函数用于触发接下来的 promise 的执行。run函数中需要注意的一点是,需要异步执行相关的回调函数。run函数的定义如下:

 function run(promise) {// `then`方法中也会调用,所以此处仍需做一次判断if (promise._status === 'pending') {return;}var value = promise._value;var callbacks = promise._status === 'fullfilled'? promise._doneCallbacks: promise._failCallbacks;// Promise需要异步操作setTimeout(function () {for (var i = 0, len = callbacks.length; i < len; i++) {callbacks[i](value);}});// 每个promise只能被执行一次。虽然`_doneCallbacks`和`_failCallbacks`用户不应该直接访问,// 但还是可以访问到,保险起见,做清空处理。promise._doneCallbacks = [];promise._failCallbacks = [];}

  run函数中调用的 callback,就是前面闭包函数makeCallback返回的函数。makeCallback函数是整个代码中较复杂的部分。其实现过程基本是按照 Promises/A+规范(中文)(英文参见 Promises/A+规范(英文))中的[Promise 解决过程]部分来完成的。可参照规范部分,此处就不具体介绍了。

  function makeCallback(promise, callback, action) {return function promiseCallback(value) {// 如果传递了callback,则使用前一个promise传递过来的值作为参数调用callback,// 并根据callback的调用结果来处理当前promiseif (typeof callback === 'function') {var x;try {x = callback(value);}catch (e) {// 如果调用callback时抛出异常,则直接用此异常对象reject当前promisereject(promise, e);}// 如果callback的返回值是当前promise,为避免造成死循环,需要抛出异常// 根据Promise+规范,此处应抛出TypeError异常if (x === promise) {var reason = new TypeError('TypeError: The return value could not be same with the promise');reject(promise, reason);}// 如果返回值是一个Promise对象,则当返回的Promise对象被resolve/reject后,再resolve/reject当前Promiseelse if (x instanceof Promise) {x.then(function (data) {resolve(promise, data);},function (reason) {reject(promise, reason);});}else {var then;(function resolveThenable(x) {// 如果返回的是一个Thenable对象(此处逻辑有点坑,参照Promise+的规范实现)if (x && (typeof x === 'object'|| typeof x === 'function')) {try {then = x.then;}catch (e) {reject(promise, e);return;}if (typeof then === 'function') {// 调用Thenable对象的`then`方法时,传递进去的`resolvePromise`和`rejectPromise`方法(及下面的两个匿名方法)// 可能会被重复调用。但Promise+规范规定这两个方法有且只能有其中的一个被调用一次,多次调用将被忽略。// 此处通过`invoked`来处理重复调用var invoked = false;try {then.call(x,function (y) {if (invoked) {return;}invoked = true;// 避免死循环if (y === x) {throw new TypeError('TypeError: The return value could not be same with the previous thenable object');}// y仍有可能是thenable对象,递归调用resolveThenable(y);},function (e) {if (invoked) {return;}invoked = true;reject(promise, e);});}catch (e) {// 如果`resolvePromise`和`rejectPromise`方法被调用后,再抛出异常,则忽略异常// 否则用异常对象reject此Promise对象if (!invoked) {reject(promise, e);}}}else {resolve(promise, x);}}else {resolve(promise, x);}}(x));}}// 如果未传递callback,直接用前一个promise传递过来的值resolve/reject当前Promise对象else {action === 'resolve'? resolve(promise, value): reject(promise, value);}};}

  至此,Promise 的实现过程大致就介绍完了,当然还有一些细节,如 Promise 中一般会提供donefail方法(可能是其它命名,不要在意这些细节),用于在不需要考虑成功或失败处理时调用,其实就是then方法的简略形式;还有一般还会提供 Promise 构造函数上的静态方法resolvereject用于直接返回一个被 resolved 或 rejected 的 promise 对象;另外,还会提供raceall静态方法,用于处理当一组 promise 中任意一个完成和全部都完成情况时的情况。具体代码可参考 我的Promise实现。

  之前比较懒,第一次开始写 blog。虽然花了不少时间,但依然觉得写得和狗屎一样烂,再接再厉吧。


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

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

相关文章

php类常量的特点,php类常量是什么?类常量用法详解

这篇文章主要介绍了php类常量用法,实例分析了php中类常量的概念、特性与相关使用技巧,需要的朋友可以参考下本文实例讲述了php类常量用法。分享给大家供大家参考。具体如下&#xff1a;类常量属于类自身&#xff0c;不属于对象实例&#xff0c;不能通过对象实例访问子类可以重写…

手机钉钉在进行视频会议时怎么录屏

https://www.iefans.net/info/v1037168.html 钉钉在进行视频会议时怎么录屏 编辑&#xff1a;秩名2020-03-24 10:14:52 钉钉 类型&#xff1a;效率办公 语言&#xff1a;简体中文 安卓下载 扫一扫下载游戏 钉钉是一款很好用的学习办公软件&#xff0c;它的工呢很多&#xf…

文件上传与下载----SpringMVC

文件上传 1、导入文件上传的jar包&#xff0c;commons-fileupload &#xff0c; Maven会自动帮我们导入他的依赖包 commons-io包&#xff1b; <!--文件上传--> <dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupl…

Vue3学习(后端开发)

目录 一、安装Node.js 二、创建Vue3工程 三、用VSCode打开 四、源代码目录src 五、入门案例——手写src 六、测试案例 七、ref和reactive的区别 一、安装Node.js 下载20.10.0 LTS版本 https://nodejs.org/en 使用node命令检验安装是否成功 node 二、创建Vue3工程 在…

微软Ignite大会约起来

今年的微软Ignite技术大会今天开始了&#xff0c;要好好学习哦&#xff0c;提供直播地址&#xff0c;通过阅读原文链接可以直达直播地址 http://soft.zdnet.com.cn/special/microsoft_ignite_2016。 大会亮点 创新 IT 技术飞速发展促发了更多行业创新&#xff0c;因此您和您的企…

aria2c rpc php,aria2c 的基本配置,附带傻瓜式源码

经常需要配置&#xff0c;但是 每次都需要查找配置项的意义&#xff0c;所以索性写在这里&#xff0c;以便有个记录&#xff0c;下次无需查找。aria2c -d/Users/blueboz/Downloads \-c \-D \-laria.log \-j5 -k1M \-x16 -s16 \--file-allocationnone \--enable-rpc \--load-coo…

探讨JS合并两个数组的方法

转载自 探讨JS合并两个数组的方法我们在项目过程中&#xff0c;有时候会遇到需要将两个数组合并成为一个的情况。 比如&#xff1a; var a [1,2,3]; var b [4,5,6];有两个数组a、b&#xff0c;需求是将两个数组合并成一个。方法如下&#xff1a; 1、concat js的Array对象提供…

最全Windows下搭建go语言开发环境以及开发IDE

https://www.cnblogs.com/ynhmonster/p/8335797.html GO语言开发环境的搭建---Windows环境下 1、Golang下载 我是通过Golang中国下载的&#xff0c;因为去官网下载十分慢&#xff0c;甚至没有进度条。 下载地址&#xff1a; https://www.golangtc.com/download 我选择的是go1…

Mybatis+mysql动态分页查询数据案例——Mybatis的配置文件(mybatis-config.xml)

<?xml version"1.0" encoding"UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration><typeAliases><!-- 动…

外媒:微信小程序顺应“APP中启动APP”的行业潮流

BI中文站 11月30日报道 上周&#xff0c;中国网络巨头腾讯的高级副总裁张小龙对外披露了一些照片&#xff0c;显示聊天工具微信开始整合“小程序”。这一功能可以让微信的用户在无需下载软件的基础上&#xff0c;使用各种互联网应用服务&#xff0c;极大扩展微信的功能。 据外媒…

SpringMVC(笔记)

MVC简介 普通的web项目每次都要进行手动的把jar包导进去&#xff0c;否则会报500&#xff0c;class not found [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VstjHhuz-1609824493673)(C:\Users\王东梁\AppData\Roaming\Typora\typora-user-images…

php 去掉url中的index.php,php 去掉url中的index.php

php去掉url中的index.php的方法&#xff1a;首先打开相应的代码文件&#xff1b;然后将if代码块嵌套在server代码块中&#xff1b;最后重启nginx服务器即可。本文操作环境&#xff1a;nginx1.0.4系统、PHP7.1版&#xff0c;DELL G3电脑nginx服务器去掉url中的index.php将if代码…

在ASP.NET Core中使用百度在线编辑器UEditor

0x00 起因 最近需要一个在线编辑器&#xff0c;之前听人说过百度的UEditor不错&#xff0c;去官网下了一个。不过服务端只有ASP.NET版的&#xff0c;如果是为了能尽快使用&#xff0c;只要把ASP.NET版的服务端作为应用部署在IIS上就可以立即使用了。不过我的需求并不急&#xf…

php 编写线程教程,php 实现多线程

通过php的Socket方式实现php程序的多线程。php本身是不支持多线程的&#xff0c;那么如何在php中实现多线程呢&#xff1f;可以想一下&#xff0c;WEB服务器本身都是支持多线程的。每一个访问者&#xff0c;当访问WEB页面的时候&#xff0c;都将调用新的线程&#xff0c;通过这…

Vue的this

一、vue编译模块 &#xff08;1&#xff09;模块域中导出对象域 export default {data() {return {msg: };} }; A.function定义函数 I、模块导出对象的各关键字的属性值 如data的值 export default {props:[propA},data:function() {//经Vue转换&#xff0c;该函数属于Vu…

如何用TypeScript开发微信小程序

微信小程序来了&#xff01;这个号称干掉传统app的玩意儿虽然目前处于内测阶段&#xff0c;不过目前在应用号的官方文档里已经放出了没有内测号也能使用的模拟器了。 工具和文档可以参考官方文档&#xff1a;https://mp.weixin.qq.com/debug/wxadoc/dev/?t1477926804193 Type…

Axios实现异步通信

Axios异步通信(通信框架) <!--导入axios--> <script src"https://cdn.bootcdn.net/ajax/libs/axios/0.19.2/axios.min.js"></script>Axios是一个开源的可以用在浏览器端和NodeJS 的异步通信框架&#xff0c;她的主要作用就是实现AJAX异步通信&…

Java IO: InputStream

转载自 Java IO: InputStream译文链接 作者: Jakob Jenkov 译者: 李璟(jlee381344197gmail.com) InputStream类是Java IO API中所有输入流的基类。InputStream子类包括FileInputStream&#xff0c;BufferedInputStream&#xff0c;PushbackInputStream等等。参考Java IO概述这…

检查异常和非检查异常 有空你去学一下检查异常和非检查异常

https://blog.csdn.net/weixin_39220472/article/details/81056647 Java检查异常和非检查异常,运行时异常和非运行时异常的区别 灰太狼_cxh 2018-07-15 20:51:31 7131 收藏 17 展开 通常&#xff0c;Java的异常(包括Exception和Error)分为 检查异常&#xff08;checked exce…

php 向html追加元素,在PHP中存储兄弟元素的属性和内部HTML

我试图从HTML页面中搜索和存储值,所以我有一个简单的数组数组。它只有2个数组,每个数组有3个项目长。我是这样定义的;这些只是标题:$fileContents array(array(Date, Title, Link));HTML具有以下结构:06/08/2018My Title 这个结构重复几次。我只需要上面的第一个(最新的)。我可…