ES6入门八:Promise异步编程与模拟实现源码
- Promise的基本使用入门:
- ——实例化promise对象与注册回调
- ——宏任务与微任务的执行顺序
- ——then方法的链式调用与抛出错误(throw new Error)
- ——链式调用的返回值与传值
- Promise的基本使用进阶:
- ——then、catch、finally的使用
- ——all、race的使用
- Promise的实现目的
- ——链式调用解决回调地狱
- ——异步回调现在与未来任务分离
- ——信任问题(控制反转):调用过早、调用过晚(不被调用)、调用次数过少过多、未能传递环境和参数、吞掉出现的错误和异常
- Promise的实现原理与模拟实现源码
一、Promise的基本使用入门
1.Promise是什么?
Promise是用来实现JS异步管理的解决方案,通过实例化Promise对象来管理具体的JS异步任务。
从Promise管理回调状态的角度来看,Promise又通常被称为状态机,在Promise拥有三种状态:pending、fulfilled、rejected。
用Promise解决JS异步执行的逻辑来理解,可以说Promise是一个未来的事件,也就是说Promise管理的任务并不是在JS的同步线程上立即执行,而是会等待同步线程的程序执行完以后才会执行。
2.创建Promise实例管理一个异步事件,通过then添加(注册)异步任务:
- let oP = new Promise((resolve, reject) => {
- setTimeout(() => {
- //使用定时器开启异步任务,使用随机数模拟异步任务受理或者拒绝
- // (数字大于60表示异步任务成功触发受理,反之失败拒绝)
- Math.random() * 100 > 60 ? resolve("ok") : reject("no");
- },1000);
- });
- oP.then((val) => {
- console.log("受理:" + val);
- },(reason) => {
- console.log("拒绝:" + reason);
- });
通过示例可以看到Promise实例化对象需要传入一个excutor函数作为参数,这个函数有两个形参resolve、reject分别表示受理事件与拒绝事件。这两个事件也就是通过实例对象调用then方法传入的两个函数,也就是说then方法传入的两个函数分别表示resolve与reject。
3.微任务与宏任务:
在js线程中,XMLHttpRequest网络请求响应事件、浏览器事件、定时器都是宏任务,会被统一放到一个任务队列中(用task queue1表示)。
而由Promise产生的异步任务resolve、reject被称为微任务(用task queue2表示)。
这两种任务的区别就是当异步任务队列中即有宏任务又有微任务时,无论宏任务比微任务早多久添加到任务队列中,都是微任务先执行,宏任务后执行。
来看下面这个示例,了解Promose微任务与宏任务的执行顺序:
- setTimeout(function(){
- console.log(0); //这是个宏任务,被先添加到异步任务队列中
- },0);
- let oP = new Promise((resolve, reject) => {
- resolve(1);//这是个微任务,被后添加到异步任务队列中
- console.log(2);//这是第一个同步任务,最先被打印到控制台
- });
- oP.then((val) => {
- console.log(val);
- },null);
- console.log(3);//这也是个同步任务,第二个被打印到控制台
测试的打印结果必然是:2 3 1 0;这就是微任务与宏任务的区别,从这一点可以了解到,JS自身实现的Promise是一个全新的功能并非语法糖,所以除原生promise以外的promise实现都是一种模拟实现,在模拟实现中基本上都是使用setTimeout来实现Promise异步任务的,所以如果不支持原生Promise浏览器使用的是兼容的Promise插件,其Promise异步任务是宏任务,在程序执行时可能会出现与新版本浏览器原生的Promise实现的功能会有些差别,这是需要注意的一个小问题。
4.Promise中的then的链式调用与抛出错误:
- let oP = new Promise((resolve, reject) => {
- setTimeout(() => {
- //使用定时器开启异步任务,使用随机数模拟异步任务受理或者拒绝
- // (数字大于60表示异步任务成功触发受理,反之失败拒绝)
- Math.random() * 100 > 60 ? resolve("ok") : reject("no");
- },1000);
- });
- oP.then((val) => {
- console.log("受理:" + val);
- },(reason) => {
- console.log("拒绝:" + reason);
- }).then((val) => {
- console.log("then2 受理:" + val);
- },(reason) => {
- console.log("then2 拒绝:" + reason);
- });
在上面的示例中,第一个then肯定出现两种情况,受理或者拒绝,这个毫无疑问,但是上面的链式调用代码中第二个then注册的resolve与reject永远都只会触发受理,所以最后的执行结果是:
- //第一种情况:
- 受理:ok
- then2 受理:undefined
- //第二种情况:
- 拒绝:no
- then2 受理:undefined
ES6中的Promise实现的then的链式调用与jQuery的Deferred.then的链式调用是有区别的,jQuery中实现的链式调用的第一个then的受理或者拒绝回调被调用后,后面的then会相应的执行受理或者拒绝。但是ES6中的Promise除第一个then以外后面都是调用受理,这里不过多的讨论jQuery的Deferred的实现,但是这是一个需要注意的问题,毕竟ES6的Promise是总结了前人的经验的基础上设计的新功能,在使用与之前的相似的功能时容易出现惯性思维。
ES6中的Promise.then链式调用的正确姿势——抛出错误:
这种设计的思考逻辑是:Promise1管理异步任务___>受理 Promise1没有抛出错误:Promise2受理
___>Promise2管理Promise1___>
___>拒绝 Promise2抛出错误:Promise2拒绝
所以前面的示例代码在拒绝中应该添加抛出错误才是正确的姿势:
- let oP = new Promise((resolve, reject) => {
- setTimeout(() => {
- Math.random() * 100 > 60 ? resolve("ok") : reject("no");
- },1000);
- });
- oP.then((val) => {
- console.log("受理:" + val);
- },(reason) => {
- console.log("拒绝:" + reason);
- throw new Error("错误提示...");
- }).then((val) => {
- console.log("then2 受理:" + val);
- },(reason) => {
- console.log("then2 拒绝:" + reason);
- });
在第一个then注册的reject中抛出错误,上面的示例的执行结果就会是这样了:
- //第一种情况:
- 受理:ok
- then2 受理:undefined
- //第二种情况:
- 拒绝:no
- then2 拒绝:Error: 错误提示...
好像这样的结果并不能说明之前的设计思考逻辑,仅仅只能说明then的链式调用在reject中抛出错误才能触发后面的reject,但是在我们的开发中必然会有即便异步正确受理,但不代表受理回调就能正确的执行完,受理的代码也可能会出现错误,所以在第一个then中受理回调也抛出错误的话同样会触发后面链式注册的reject,看示例:
- let oP = new Promise((resolve, reject) => {
- setTimeout(() => {
- //使用定时器开启异步任务,使用随机数模拟异步任务受理或者拒绝
- // (数字大于60表示异步任务成功触发受理,反之失败拒绝)
- Math.random() * 100 > 60 ? resolve("ok") : reject("no");
- },1000);
- });
- oP.then((val) => {
- console.log("受理:" + val);
- if(Math.random() * 100 > 30){
- return "then1受理成功执行完毕!";
- }else{
- throw new Error("错误提示:then1受理没有成功执行完成。");
- }
- },(reason) => {
- console.log("拒绝:" + reason);
- throw new Error("错误提示...");
- }).then((val) => {
- console.log("then2 受理:" + val);
- },(reason) => {
- console.log("then2 拒绝:" + reason);
- });
这时候整个示例最后的执行结果就会出现三种情况:
- //情况1:
- 受理:ok
- then2 受理:then1受理成功执行完毕!
- //情况2:
- promise.html:24 拒绝:no
- then2 拒绝:Error: 错误提示...
- //情况3:
- 受理:ok
- then2 拒绝:Error: 错误提示:then1受理没有成功执行完成。
5.then方法链式调用的返回值与传值:
在前面的代码中,相信你已经发现,如果前面then注册的回调不返回值或者不抛出错误,后面的then接收不到任何值,打印出来的参数为undefined。这一点也与jQuery中的then有些区别,在jQuery中如果前面的then没有返回值,后面then注册的回调函数会继续使用前面回调函数接收的参数。
在前面的示例中已经有返回值、抛出错误和传值的展示了,这里重点来看看如果返回值是一个Promise对象,会是什么结果:
- let oP = new Promise((resolve, reject) => {
- setTimeout(() => {
- //使用定时器开启异步任务,使用随机数模拟异步任务受理或者拒绝
- // (数字大于60表示异步任务成功触发受理,反之失败拒绝)
- Math.random() * 100 > 60 ? resolve("ok") : reject("no");
- },1000);
- });
- oP.then((val) => {
- console.log("受理:" + val);
- return new Promise((resolve, reject) => {
- Math.random() * 100 > 60 ? resolve("then1_resolve_ok") : reject("then_resolve_no");
- });
- },(reason) => {
- console.log("拒绝:" + reason);
- return new Promise((resolve, reject) => {
- Math.random() * 100 > 60 ? resolve("then1_reject_ok") : reject("then1_reject_no");
- })
- }).then((val) => {
- console.log("then2 受理:" + val);
- },(reason) => {
- console.log("then2 拒绝:" + reason);
- });
以上的示例出现的结果会有四种:
- //情况一、二
- 受理:ok
- then2 受理:then1_resolve_ok / then2 拒绝:then1_resolve_no
- //情况三、四
- 拒绝:no
- then2 受理:then1_reject_ok / then2 拒绝:then1_reject_no
通过示例可以看到,当前面一个then的回调返回值是一个Promise对象时,后面的then触发的受理或者拒绝是根据前面返回的Promise对象触发的受理或者拒绝来决定的。
二、Promise的基本使用进阶
1.在Promise中标准的捕获异常的方法是catch,虽然前面的示例中使用了reject拒绝的方式捕获异常,但一般建议使用catch来实现捕获异常。需要注意的是异常一旦被捕获就不能再次捕获,意思就是如果在链式调用中前面的reject已经捕获了异常,后面链式调用catch就不能再捕获。
建议使用catch异常捕获的代码结构:
- let oP = new Promise((resolve, reject) => {
- setTimeout(() => {
- //使用定时器开启异步任务,使用随机数模拟异步任务受理或者拒绝
- // (数字大于60表示异步任务成功触发受理,反之失败拒绝)
- Math.random() * 100 > 60 ? resolve("ok") : reject("no");
- },1000);
- });
- oP.then((val) => {
- console.log("受理:" + val);
- },(reason) => {
- console.log("拒绝:" + reason);
- throw new Error("错误提示...");
- }).then((val) => {
- console.log("then2 受理:" + val);
- }).catch((err) => {
- console.log(err);
- })
catch不能捕获的情况:
- let oP = new Promise((resolve, reject) => {
- setTimeout(() => {
- //使用定时器开启异步任务,使用随机数模拟异步任务受理或者拒绝
- // (数字大于60表示异步任务成功触发受理,反之失败拒绝)
- Math.random() * 100 > 60 ? resolve("ok") : reject("no");
- },1000);
- });
- oP.then((val) => {
- console.log("受理:" + val);
- },(reason) => {
- console.log("拒绝:" + reason);
- throw new Error("错误提示...");
- }).then((val) => {
- console.log("then2 受理:" + val);
- }, (reason) => {
- console.log("then2 拒绝:" + reason);
- }).catch((err) => {
- console.log("异常捕获:",err);
- })
- //这种情况就只能是在第二个then中的reject捕获异常,catch不能捕获到异常
一个非技术性的问题,调用了一个空的then会被忽视,后面的then或者catch依然正常执行:
- let oP = new Promise((resolve, reject) => {
- setTimeout(() => {
- //使用定时器开启异步任务,使用随机数模拟异步任务受理或者拒绝
- // (数字大于60表示异步任务成功触发受理,反之失败拒绝)
- Math.random() * 100 > 60 ? resolve("ok") : reject("no");
- },1000);
- });
- oP.then((val) => {
- console.log("受理:" + val);
- },(reason) => {
- console.log("拒绝:" + reason);
- throw new Error("错误提示...");
- })
- .then()//这个then会被忽视,如果前面一个then调用了reject拒绝,后面的catch能正常捕获(或者后面链式调用then都能正常执行)
- .catch((err) => {
- console.log("异常捕获:",err);
- });
2.在Promise方法中,除了finally都会继续返回Promise对象,而且finally传入的回调函数一定会被执行,这个跟前面的一种情况非常类似,就是当前面的then不抛出错误的时候,后面的then一定是调用受理,实际上底层的实现也就是同一个逻辑上实现的。只是finally不再返回Promise对象,但需要注意的是finally注册的回调函数获取不到任参数。
- let oP = new Promise((resolve, reject) => {
- setTimeout(() => {
- //使用定时器开启异步任务,使用随机数模拟异步任务受理或者拒绝
- // (数字大于60表示异步任务成功触发受理,反之失败拒绝)
- Math.random() * 100 > 60 ? resolve("ok") : reject("no");
- },1000);
- });
- oP.then((val) => {
- console.log("受理:" + val);
- },(reason) => {
- console.log("拒绝:" + reason);
- throw new Error("错误提示...");
- }).catch((err) => {
- console.log("异常捕获:",err);
- }).finally(() => {
- console.log("结束");//这个回调函数接收不到任何参数
- })
3.Promise给并发处理提供了两种实现方式all、race,这两个的处理逻辑非常类似条件运算符的与(&&)或 (||)运算,all就是用来处理当多个Promise全部成功受理就受理自身的受理回调resolve,否则就拒绝reject。race的处理多个Promise只需要一个Promise成功受理就触发自身的受理回调,否则就拒绝reject。它们处理Promise实例的方式都是将Promise实例对象作为数组元素,然后将包裹的数组作为all或race的参数进行处理。
这里使用一段nodejs环境读取文件代码来展示Promise.all的使用:
- //路径+文件名: 内容:data
- ./data/number.txt "./data/name.txt"
- ./data/name.txt "./data/score.tet"
- ./data/score/txt "99"
- //src目录结构
- --index.js
- --data
- ----number.txt
- ----name.txt
- ----score.txt
Promise.all实现文件数据并发读取:
- let fs = require('fs');
- function readFile(path){
- return new Promise((resolve,reject) => {
- fs.readFile(path,'utf-8', (err,data) => {
- if(data){
- resolve(data);
- }else{
- reject(err);
- }
- });
- });
- }
- Promise.all([readFile("./data/number.txt"),readFile("./data/name.txt"),readFile("./data/score.txt")]).then((val) =>{
- console.log(val);
- });
在nodejs环境中执行代码,打印结果:
- node index.js //执行js文件
- [ './data/name.txt', './data/score.txt', '99' ] //打印结果
从示例中可以看到Promise.all获取的值是全部Promise实例受理回调传入的值,并且以数组的方式传入。
接着来看一个Promise.race的示例,这个示例:
- var op1 = new Promise((resolve, reject) => {
- setTimeout(resolve, 500, "one");
- });
- var op2 = new Promise((resolve, reject) => {
- setTimeout(resolve, 100, "two");
- });
- Promise.race([op1,op2]).then((val) => {
- console.log(val);
- });
- //打印结果:two
Promise.race获的值是第一个Promise实例受理回调传入的值。
4.Promise.all与Promise.race的传值规则:
all:
所有Promise实例受理resolve,即所有异步回调成功的情况下,将所有Promise实例的resolve接收的参数合并成一个数组,传递给Promise.all生成的新的Promise实例的resolve回调处理。
如果有一个失败的情况下,即Promise.all生成的新的Promise实例触发回调reject函数,这个函数会接收到最先失败的Promise实例通过reject回调传入的参数。
race:
通过Promise.race处理的Promise实例中最先获得结果的Promise实例的参数,传递给Promise.race产生的Promise实例,不论成功与失败,成功就出发resolve函数,失败就出发reject函数。
三、Promise的实现目的
1.链式调用解决回调地狱:在一开始学习编程的时候我们一定都写过一连串的作用域嵌套代码,来解决一些业务逻辑链相对比较长的功能,然后还可能跟同学炫耀“你看我把这个功能写出来了,还能正确执行”。不要问我为什么这么肯定,这种事我做过,我的同学和朋友也有做过。这为什么值得炫耀呢?无非就是面对这种业务逻辑链比较长的功能很难保证在那个不环节不出错,所以能驾驭层层嵌套的代码的确可以说很“认真”的在编码。我在jQuery的ajax的一篇博客中就是用了一个非常详细的案例展示了回调地狱:jQuery使用(十二):工具方法之ajax的无忧回调(优雅的代码风格)
这里我使用第二节中的(3:Promise.all)案例,(应用之前的文件结构)来写一个文件层级读取的示例:
- //这是一个基于nodejs环境的js示例,请在nodejs环境中执行index.js
- let fs = require('fs');
- fs.readFile("./data/number.txt","utf-8",(err,data) => {
- if(data){
- fs.readFile(data,"utf-8",(err,data) => {
- if(data){
- fs.readFile(data,"utf-8",(err,data) => {
- console.log(data);
- })
- }
- })
- }
- });
相信大家遇到这种代码都会知道这样的代码结构,不易于维护,编写容易出错并且还不容易追踪错误。下面来看看使用Promise如何回避这样的问题,来提高代码质量:
- //这是一个基于nodejs环境的js示例,请在nodejs环境中执行index.js
- let fs = require('fs');
- function readFile(path){
- return new Promise((resolve,reject) => {
- fs.readFile(path,'utf-8', (err,data) => {
- if(data){
- resolve(data);
- }else{
- reject(err);
- }
- });
- });
- }
- readFile("./data/number.txt").then((val) => {
- return readFile(val);//这里去获取nama.text的文本数据
- },(reason) => {
- console.log(reason);
- }).then((val) => {
- return readFile(val);//这里去获取score.text的文本数据
- },(reason) => {
- console.log(reason);
- }).then((val) => {
- console.log(val);//这里最后打印score.text的文本数据
- },(reason) => {
- console.log(reason);
- });
2.异步回调现在与未来任务分离:
Kyle Simpson大神在《你不知道的js中卷》的第二部分第一章(1.3并行线程)中给我说明了一个我长期混洗的知识点,“异步”与“并行”,他明确的阐述了异步是关于现在和将来的事件间隙,而并非关于能同时发生的事情。
简单来说,在js中我们可以把同步任务理解为现在要执行的任务,异步则是将来要执行的任务,个人认为这是Promise的核心功能,Promise的then本质上就是这样的设计思路,在实例化的Promise对象的时候就已经调用了回调任务resolve或者reject,但是Promise将这两个回调任务处理成了异步(微任务)模式,通过前面的应用介绍我们知道Promise实例化的时候并没有添加这两个任务,而是后面基于同步任务的then添加的,所以resolve和reject才能在未来有真正的任务可以执行。
利用异步的这种现在与未来的异步设计思路实现了Promise.all和Promise.race,解决了前端回调的竞态问题。关于js竞态问题可以了解《你不知道的js中卷》第二部分第一章和第三章的3.1。(这给内容可多可少,但是想想Kyle Simpson的清晰明了的分析思路,建议大家去看他书。)
3.信任问题(控制反转):
相信大家在应用js开发的时候都使用果类似这样的代码:
- ajax("...",function(...){ ... })
通常这样的代码我们都会想到插件或者第三方库,如果这是一个购物订单,你知道这段代码存在多大的风险吗?我们根本就不知道这个回调函数会被执行多少次,因为怎么执行是由别让人的插件和库来控制的。顺着这个思路,在《你不知道的js中卷》的第二部分第二章2.3.1最后,大神提出这样的追问:调用过早怎么办?调用过晚怎么办?调用多次或者次数太少怎么办?没有传递参数或者环境怎么办?出现错误或者异常怎么办?这些内容在《你不知道的js中卷》第二部分第三章3.3都详细的描述了基于Promise的解决方案。
本质上也就是Promise的控制反转的设计模式,比如前面的ajax()请求可以这样来写:
- var oP = new Promise((resolve,reject) => {
- resolve(...);
- });
- oP.then((val) => {
- ajax("...",function(...){...});
- });
我们知道,每个Promise只能决议一次,无论成功或者失败,所以就不用当心一个购物订单请求会不会被插件或者第三方库误操作发送多次(这并不是绝对的,毕竟ajax回调函数内部怎么执行还是别人的代码,这里我能只能假设ajax回调函数是可信任的)。
关于Promise的实现目的还有很多,我也只能在这里列举一些比较典型的和常见的问题,如果想了解更多我首先建议大家去看我前面多次提到的书,或者到各大技术论坛了解他人的研究和发现,下面接着进入激动人心的Promise源码部分。
四、Promise的实现原理与模拟实现源码
Promise实现标准文档:https://promisesaplus.com
由于源码的复杂性还算比较高,我们采用分阶段实现的方式,从Promise的一部分功能开始然后逐渐完成所有功能。
第一阶段:基于Promise的三种状态:pending、Fulfilled、Rejected实现同步的状态决议回调任务处理;
第二阶段:基于阶段一的状态机实现Promise异步的状态决议回调任务处理;
第三阶段:实现then的链式调用;
第四阶段:使用setTimeout模拟实现Promise异步回调任务、处理回调任务中的异常、忽略链式调用中的空then;
第五阶段:实现回调函数返回Promise实例;
第六阶段:实现Promise静态方法race、all;
第七阶段:实现Promise原型方法catch、finally、以及扩展一个deferred静态方法
1.原理分析之状态:
Promise实例三种状态:pending、Fulfilled、Rejected,当pending状态时表示未决议,可转换成Fulfilled或者Rejected状态,转换状态后不可更改。
Promise实例化时执行excutor函数,并使用try...catch处理excutor可能抛出的错误行为,如果抛出错误,将状态设置为Rejected(拒绝)。
在原型上定义then方法,实现回调任务处理。
- function myPromise(excutor){
- var self = this;
- self.status = "pending";
- self.resolveValue = null; //缓存受理回调的参数
- self.rejectReason = null; //缓存拒绝回调的参数
- function resolve(value){
- if(self.status === "pending"){
- self.status = "Fulfilled";
- self.resolveValue = value; // 将受理回调执行的参数缓存到Promise实例属性上
- }
- }
- function reject(reason){
- if(self.status === "pending"){
- self.status = "Rejected";
- self.rejectReason = reason; // 将拒绝回调执行的参数缓存到Promise实例属性上
- }
- }
- //当excutor抛出错误执行reject
- try{
- excutor(resolve,reject);
- }catch(e){
- reject(e);
- }
- };
- myPromise.prototype.then = function(onFulfilled,onRejected){
- var self = this;
- if(self.status === "Fulfilled"){
- onFulfilled(self.resolveValue);
- }
- if(self.status === "Rejected"){
- onRejected(self.rejectReason);
- }
- }
测试代码:
- var myP = new myPromise((resolve,reject) => {
- // 测试resolve
- // resolve("受理");
- // 测试reject
- // reject("拒绝");
- // 测试抛出错误
- throw new Error("excutor抛出错误");
- });
- myP.then((val) => {
- console.log(val);
- }, (reason) => {
- console.log(reason);
- });
2.Promise原理分析之异步:
这部分还不是解析Promise微任务的内容,而是解析当excutor内决议是一个异步任务,比如ajax请求的回调任务,这种情况就是then的注册行为会在状态变化之前,所以需要将注册回调函数缓存下来,等到异步任务执行时调用。
- function myPromise(excutor){
- var self = this;
- self.status = "pending";
- self.resolveValue = null; //缓存受理回调的参数
- self.rejectReason = null; //缓存拒绝回调的参数
- self.ResolveCallBackList = []; //当Promise是一个异步任务时,缓存受理回调函数
- self.RejectCallBackList = []; //当Promise是一个异步任务时,缓存拒绝回调函数
- function resolve(value){
- if(self.status === "pending"){
- self.status = "Fulfilled";
- self.resolveValue = value; // 将受理回调执行的参数缓存到Promise实例属性上
- self.ResolveCallBackList.forEach(function(ele){
- //这里当excutor内是同步任务时,ResolveCallBackList没有元素,当excutor内是一个异步任务时就会执行then缓存的受理回调函数
- ele();
- });
- }
- }
- function reject(reason){
- if(self.status === "pending"){
- self.status = "Rejected";
- self.rejectReason = reason; // 将拒绝回调执行的参数缓存到Promise实例属性上
- self.RejectCallBackList.forEach(function(ele){
- //这里当excutor内是同步任务时,RejectCallBackList没有元素,当excutor内是一个异步任务时就会执行then缓存的拒绝回调函数
- ele();
- });
- }
- }
- //当excutor抛出错误执行reject
- try{
- excutor(resolve,reject);
- }catch(e){
- reject(e);
- }
- };
- myPromise.prototype.then = function(onFulfilled,onRejected){
- var self = this;
- if(self.status === "Fulfilled"){
- onFulfilled(self.resolveValue);
- }
- if(self.status === "Rejected"){
- onRejected(self.rejectReason);
- }
- // 当excutor执行时,内部回调是一个异步任务,Promise的状态不会发生改变
- // 所以异步作为一个将来任务,先缓存到Promise实例对象上
- if(self.status === "pending"){
- self.ResolveCallBackList.push(function(){
- onFulfilled(self.resolveValue);
- });
- self.RejectCallBackList.push(function(){
- onRejected(self.rejectReason);
- })
- }
- }
测试代码:(异步任务的出现异常报错这部分还没处理,所以只测试异步任务的受理或拒绝)
- var myP = new myPromise((resolve,reject) => {
- setTimeout(() => {
- // 测试resolve
- resolve("受理");
- // 测试reject
- // reject("拒绝");
- },1000);
- });
- myP.then((val) => {
- console.log(val);
- }, (reason) => {
- console.log(reason);
- });
3.then的链式调用:
在ES6的Promise中,then的链式调用是返回一个全新的Promise实例,这一点在前面的应用中已经有说明,链式调用中除了返回一个全新的Promise对象以外,还有一个关键的问题就是将前面的Promise的的返回值,作为参数传给后面一个Promise实例的回调函数使用。
这个阶段暂时不处理返回Promise实例的相关内容,所以还记得我在使用的第一节第四小点,这里测试第一个Promise实例的resolve和reject第二个then注册受理和拒绝只会触发受理。
所以这样作为一个基本链式调用实现就非常的简单了,因为Promise实例化时需要执行一个同步的回调函数excutor,我们都知道,then的回调注册时同步进行,所以我们只需要将then的注册放到需要心生成的Promise实例化时同步执行excutor中,然后获取前一个Promise的回调执行返回值,作为新生成的Promise实例回调的参数传入即可,这个说起来好像有点复杂,但是实现非常的简单,建议直接看代码:
- function myPromise(excutor){
- var self = this;
- self.status = "pending";
- self.resolveValue = null; //缓存受理回调的参数
- self.rejectReason = null; //缓存拒绝回调的参数
- self.ResolveCallBackList = []; //当Promise是一个异步任务时,缓存受理回调函数
- self.RejectCallBackList = []; //当Promise是一个异步任务时,缓存拒绝回调函数
- function resolve(value){
- if(self.status === "pending"){
- self.status = "Fulfilled";
- self.resolveValue = value; // 将受理回调执行的参数缓存到Promise实例属性上
- self.ResolveCallBackList.forEach(function(ele){
- //这里当excutor内是同步任务时,ResolveCallBackList没有元素,当excutor内是一个异步任务时就会执行then缓存的受理回调函数
- ele();
- });
- }
- }
- function reject(reason){
- if(self.status === "pending"){
- self.status = "Rejected";
- self.rejectReason = reason; // 将拒绝回调执行的参数缓存到Promise实例属性上
- self.RejectCallBackList.forEach(function(ele){
- //这里当excutor内是同步任务时,RejectCallBackList没有元素,当excutor内是一个异步任务时就会执行then缓存的拒绝回调函数
- ele();
- });
- }
- }
- //当excutor抛出错误执行reject
- try{
- excutor(resolve,reject);
- }catch(e){
- reject(e);
- }
- };
- myPromise.prototype.then = function(onFulfilled,onRejected){
- var self = this;
- var nextPromise = new myPromise(function (resolve,reject) {
- if(self.status === "Fulfilled"){
- var nextResolveValue = onFulfilled(self.resolveValue);
- resolve(nextResolveValue);//将获取的前一个Promise回调任务的返回值传给新生成的Promise实例的受理回调任务
- }
- if(self.status === "Rejected"){
- var nextRejectValue = onRejected(self.rejectReason);
- resolve(nextRejectValue);//将获取的前一个Promise回调任务的返回值传给新生成的Promise实例的受理回调任务
- }
- // 当excutor执行时,内部回调是一个异步任务,Promise的状态不会发生改变
- // 所以异步作为一个将来任务,先缓存到Promise实例对象上
- if(self.status === "pending"){
- self.ResolveCallBackList.push(function(){
- var nextResolveValue = onFulfilled(self.resolveValue);
- resolve(nextResolveValue);//将获取的前一个Promise回调任务的返回值传给新生成的Promise实例的受理回调任务
- });
- self.RejectCallBackList.push(function(){
- var nextRejectValue = onRejected(self.rejectReason);
- resolve(nextRejectValue);//将获取的前一个Promise回调任务的返回值传给新生成的Promise实例的受理回调任务
- });
- }
- });
- return nextPromise;
- }
测试代码:
- var myP = new myPromise((resolve,reject) => {
- setTimeout(() => {
- // 测试resolve
- // resolve(0);
- // 测试reject
- reject(0);
- },1000);
- });
- myP.then((val) => {
- console.log("受理:" + val);
- return 1;
- }, (reason) => {
- console.log("拒绝:" + reason);
- return "1";
- }).then((val) => {
- console.log("受理:" + val);
- }, (reason) => {
- console.log("拒绝:" + reason);//这个暂时不会执行到
- });
4.使用setTimeout模拟实现Promise异步回调任务:
注意这种模拟实现与ES6实现的异步回调有一个根本性差异,ES6的Promise异步回调任务是微任务,但是通过setTimeout模拟实现的是宏任务。实现其实也是非常的简单,只需要将回调任务放到setTimeout的回调函数中即可,并设置延迟时间为0;
然后再在这部分实现对回调任务中抛出错误的处理,这是因为回调任务中的错误需要在下一个Promise的reject中或者catch中被捕获,所以有了链式调用的基础就可以来实现这个功能了。
还有第二节第一小点钟提到忽略链式调用中的空then(),关于这个问题我前面只在使用中说会忽略这个空then,但是实际底层的实现并非时忽略,而是将前一个Promise基于这个空的Promise实例传递给了下一个非空的Promise。这里我们先来看一段基于原生的Promise手动传递应用:
- var myP = new Promise((resolve,reject) => {
- setTimeout(() => {
- // 测试resolve
- // resolve(0);
- // 测试reject
- reject(0);
- },1000);
- });
- myP.then((val) => {
- console.log("受理:" + val);
- return 1;
- }, (reason) => {
- console.log("拒绝:" + reason);
- // 测试Error
- throw new error("这里抛出错误");
- }).then((val) => {
- return val; //将前一个受理的返回值传递给下一个Promise受理回调
- },(reason) => {
- throw new Errror(reason); //将前一个Promise抛出的错误传递给下一个Promise的拒绝回调
- })
- .then((val) => {
- console.log("受理:" + val);
- }, (reason) => {
- console.log("拒绝:" + reason);//这个暂时不会执行到
- });
实际上,Promise底层也是基于这样的传递行为来处理空then的,而且在前面的Promise应用介绍中,有一种情况没有深入的说明,就是当then(null,(...) => {...})、then((...) => {...},null)、then(null,null)进行深入的说明,请示本质上同样是使用了上面示例中的传递行为。还是那句话,说起来非常复杂,实际代码非常简单:
- function myPromise(excutor){
- var self = this;
- self.status = "pending";
- self.resolveValue = null; //缓存受理回调的参数
- self.rejectReason = null; //缓存拒绝回调的参数
- self.ResolveCallBackList = []; //当Promise是一个异步任务时,缓存受理回调函数
- self.RejectCallBackList = []; //当Promise是一个异步任务时,缓存拒绝回调函数
- function resolve(value){
- if(self.status === "pending"){
- self.status = "Fulfilled";
- self.resolveValue = value; // 将受理回调执行的参数缓存到Promise实例属性上
- self.ResolveCallBackList.forEach(function(ele){
- //这里当excutor内是同步任务时,ResolveCallBackList没有元素,当excutor内是一个异步任务时就会执行then缓存的受理回调函数
- ele();
- });
- }
- }
- function reject(reason){
- if(self.status === "pending"){
- self.status = "Rejected";
- self.rejectReason = reason; // 将拒绝回调执行的参数缓存到Promise实例属性上
- self.RejectCallBackList.forEach(function(ele){
- //这里当excutor内是同步任务时,RejectCallBackList没有元素,当excutor内是一个异步任务时就会执行then缓存的拒绝回调函数
- ele();
- });
- }
- }
- //当excutor抛出错误执行reject
- try{
- excutor(resolve,reject);
- }catch(e){
- reject(e);
- }
- };
- myPromise.prototype.then = function(onFulfilled,onRejected){
- if(!onFulfilled){ //当没有传入受理回调函数时,自动将参数传递给下一个Promise实例的受理函数作为参数
- onFulfilled = function(val){
- return val;
- }
- }
- if(!onRejected){ //当没有传入拒绝回调函数时,自动将参数传递给下一个Promise实例的拒绝函数作为参数
- // 可能这里你会疑惑,为什么要使用抛出错误的方式传递
- // 前面已经说明过,拒绝回调只有在Promise实例化中调用了拒绝回调函数以外,只有抛出错误才会会触发下一个Promise实例的拒绝回调
- onRejected = function(reason){
- throw new Error(reason);
- }
- }
- var self = this;
- var nextPromise = new myPromise(function (resolve,reject) {
- if(self.status === "Fulfilled"){
- setTimeout(function(){ //使用setTimeout模拟实现异步回调
- try{ //使用try...catch来捕获回调任务的异常
- var nextResolveValue = onFulfilled(self.resolveValue);
- resolve(nextResolveValue);//将获取的前一个Promise回调任务的返回值传给新生成的Promise实例的受理回调任务
- }catch(e){
- reject(e);
- }
- },0);
- }
- if(self.status === "Rejected"){
- setTimeout(function(){
- try{
- var nextRejectValue = onRejected(self.rejectReason);
- resolve(nextRejectValue);//将获取的前一个Promise回调任务的返回值传给新生成的Promise实例的受理回调任务
- }catch(e){
- reject(e);
- }
- },0);
- }
- // 当excutor执行时,内部回调是一个异步任务,Promise的状态不会发生改变
- // 所以异步作为一个将来任务,先缓存到Promise实例对象上
- if(self.status === "pending"){
- self.ResolveCallBackList.push(function(){
- setTimeout(function(){
- try{
- var nextResolveValue = onFulfilled(self.resolveValue);
- resolve(nextResolveValue);//将获取的前一个Promise回调任务的返回值传给新生成的Promise实例的受理回调任务
- }catch(e){
- reject(e);
- }
- },0);
- });
- self.RejectCallBackList.push(function(){
- setTimeout(function(){
- try{
- var nextRejectValue = onRejected(self.rejectReason);
- resolve(nextRejectValue);//将获取的前一个Promise回调任务的返回值传给新生成的Promise实例的受理回调任务
- }catch(e){
- reject(e);
- }
- },0);
- });
- }
- });
- return nextPromise;
- }
这部分功能已经非常接近原生Promise了,就不提供测试代码了,下面直接进入第五阶段。
5.实现回调函数返回Promise实例:
关于回调返回Promise实例与前面的空then的处理思路非常相识,在空then的情况下我们需要将值传递给下一个then生成的Promise实例。那回调返回Promise实例就是需要,将原本注册给then自身生成的Promise实例的回调重新注册给上一个Promise回调返回的Promise实例,实现代码同样非常简单:
- function myPromise(excutor){
- var self = this;
- self.status = "pending";
- self.resolveValue = null; //缓存受理回调的参数
- self.rejectReason = null; //缓存拒绝回调的参数
- self.ResolveCallBackList = []; //当Promise是一个异步任务时,缓存受理回调函数
- self.RejectCallBackList = []; //当Promise是一个异步任务时,缓存拒绝回调函数
- function resolve(value){
- if(self.status === "pending"){
- self.status = "Fulfilled";
- self.resolveValue = value; // 将受理回调执行的参数缓存到Promise实例属性上
- self.ResolveCallBackList.forEach(function(ele){
- //这里当excutor内是同步任务时,ResolveCallBackList没有元素,当excutor内是一个异步任务时就会执行then缓存的受理回调函数
- ele();
- });
- }
- }
- function reject(reason){
- if(self.status === "pending"){
- self.status = "Rejected";
- self.rejectReason = reason; // 将拒绝回调执行的参数缓存到Promise实例属性上
- self.RejectCallBackList.forEach(function(ele){
- //这里当excutor内是同步任务时,RejectCallBackList没有元素,当excutor内是一个异步任务时就会执行then缓存的拒绝回调函数
- ele();
- });
- }
- }
- //当excutor抛出错误执行reject
- try{
- excutor(resolve,reject);
- }catch(e){
- reject(e);
- }
- };
- //用来处理回调返回值的情况:当返回值为Promise时,将回调函数注册到该Promise上,如果为普通值直接执行回调函数
- function ResolutionRetrunPromise(returnValue,res,rej){
- if(returnValue instanceof myPromise){
- returnValue.then(function(val){
- res(val);
- },function(reason){
- rej(reason);
- });
- }else{
- res(returnValue);
- }
- }
- myPromise.prototype.then = function(onFulfilled,onRejected){
- if(!onFulfilled){ //当没有传入受理回调函数时,自动将参数传递给下一个Promise实例的受理函数作为参数
- onFulfilled = function(val){
- return val;
- }
- }
- if(!onRejected){ //当没有传入拒绝回调函数时,自动将参数传递给下一个Promise实例的拒绝函数作为参数
- // 可能这里你会疑惑,为什么要使用抛出错误的方式传递
- // 前面已经说明过,拒绝回调只有在Promise实例化中调用了拒绝回调函数以外,只有抛出错误才会会触发下一个Promise实例的拒绝回调
- onRejected = function(reason){
- throw new Error(reason);
- }
- }
- var self = this;
- var nextPromise = new myPromise(function (resolve,reject) {
- if(self.status === "Fulfilled"){
- setTimeout(function(){ //使用setTimeout模拟实现异步回调
- try{ //使用try...catch来捕获回调任务的异常
- var nextResolveValue = onFulfilled(self.resolveValue);
- ResolutionRetrunPromise(nextResolveValue,resolve,reject);//使用回调返回值来处理下一个回调任务
- }catch(e){
- reject(e);
- }
- },0);
- }
- if(self.status === "Rejected"){
- setTimeout(function(){
- try{
- var nextRejectValue = onRejected(self.rejectReason);
- ResolutionRetrunPromise(nextRejectValue,resolve,reject);
- }catch(e){
- reject(e);
- }
- },0);
- }
- // 当excutor执行时,内部回调是一个异步任务,Promise的状态不会发生改变
- // 所以异步作为一个将来任务,先缓存到Promise实例对象上
- if(self.status === "pending"){
- self.ResolveCallBackList.push(function(){
- setTimeout(function(){
- try{
- var nextResolveValue = onFulfilled(self.resolveValue);
- ResolutionRetrunPromise(nextResolveValue,resolve,reject);
- }catch(e){
- reject(e);
- }
- },0);
- });
- self.RejectCallBackList.push(function(){
- setTimeout(function(){
- try{
- var nextRejectValue = onRejected(self.rejectReason);
- ResolutionRetrunPromise(nextRejectValue,resolve,reject);
- }catch(e){
- reject(e);
- }
- },0);
- });
- }
- });
- return nextPromise;
- }
测试代码:
- var mP = new myPromise((resolve,reject) => {
- // resolve("受理1");
- reject("拒绝1");
- });
- mP.then((val) => {
- console.log(val);
- return new myPromise((resolve,reject) => {
- // resolve("受理2");
- // reject("拒绝2");
- });
- },(reason) => {
- console.log(reason);
- return new myPromise((resolve,reject) => {
- // resolve("受理2");
- reject("拒绝2");
- });
- }).then((val) => {
- console.log(val);
- },(reason) => {
- console.log(reason);
- })
6.实现Promise静态方法race、all:
- myPromise.race = function(promiseArr){
- return new myPromise(function(resolve,reject){
- promiseArr.forEach(function(promise,index){
- promise.then(resolve,reject);
- });
- });
- }
- //all通过给每个Promise传递一个受理回调,这个回调负责获取每个受理函数的参数,并判断是否全部受理,如果全部受理触发all自身的受理回调
- //另外将all的reject传递给每个Promise的reject,只要任意一个触发就完成all的拒绝回调
- myPromise.all = function(promiseArr){
- function gen(length,resolve){
- var count = 0;
- var values = [];
- return function(i,value){
- values[i] = value;
- if(++count === length){
- resolve(values);
- }
- }
- }
- return new myPromise(function(resolve,reject){
- let done = gen(promiseArr.length,resolve);
- promiseArr.forEach(function(promise,index){
- promise.then((val) => {
- done(index,val);
- },reject);
- })
- })
- }
7.实现Promise原型方法catch、finally、以及扩展一个deferred静态方法
- //原型方法catch的实现
- myPromise.prototype.catch = function(onRejected){
- return this.then(null,onRejected);
- }
- // 原型方法finally
- myPromise.prototype.finally = function(fun){
- fun();
- }
- //扩展静态方法deferred方法
- myPromise.deferred = function(){
- var defer = {};
- defer.promise = new Promise((resolve,reject) => {
- defer.resolve = resolve;
- defer.reject = reject;
- });
- return defer;
- }
(全文完)
ES6入门八:Promise异步编程与模拟实现源码的更多相关文章
- 【ES6】Generator+Promise异步编程
一.概念 首先我们要理解Generator和Promise的概念. Generator:意思是生成器,可以在函数内部通过yeild来控制语句的执行或暂停状态. *Foo(){ yeild consol ...
- ES6笔记(7)-- Promise异步编程
系列文章 -- ES6笔记系列 很久很久以前,在做Node.js聊天室,使用MongoDB数据服务的时候就遇到了多重回调嵌套导致代码混乱的问题. JS异步编程有利有弊,Promise的出现,改善了这一 ...
- Promise异步编程解决方案
Promise是ES6中新增的异步编程解决方案,体现在代码中它是一个对象,可以通过 Promise 构造函数来实例化. 其最基本的使用 new Promise(function(resolve,rej ...
- java 模拟qq源码
java 模拟qq源码: http://files.cnblogs.com/files/hujunzheng/QQ--hjzgg.zip
- 【并发编程】【JDK源码】J.U.C--AQS 及其同步组件(2/2)
原文:慕课网高并发实战(七)- J.U.C之AQS 在[并发编程][JDK源码]AQS (AbstractQueuedSynchronizer)(1/2)中简要介绍了AQS的概念和基本原理,下面继续对 ...
- openlayers5-webpack 入门开发系列结合 echarts4 实现散点图(附源码下载)
前言 openlayers5-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载 ...
- es6 generator函数的异步编程
es6 generator函数,我们都知道asycn和await是generator函数的语法糖,那么genertaor怎么样才能实现asycn和await的功能呢? 1.thunk函数 将函数 ...
- 学习Promise异步编程
JavaScript引擎建立在单线程事件循环的概念上.单线程( Single-threaded )意味着同一时刻只能执行一段代码.所以引擎无须留意那些"可能"运行的代码.代码会被放 ...
- promise异步编程的原理
一.起源 JavaScript中的异步由来已久,不论是定时函数,事件处理函数还是ajax异步加载都是异步编程的一种形式,我们现在以nodejs中异步读取文件为例来编写一个传统意义的异步函数: var ...
随机推荐
- Django上线部署之Apache
环境: 1.Windows Server 2016 Datacenter 64位 2.SQL Server 2016 Enterprise 64位 3.Python 3.6.0 64位 4.admin ...
- docker安装到基本使用
记录docker概念,安装及入门日常使用 Docker安装(Linux / Debian) 查看官方文档,在Debian上安装Docker,其他平台在这里查阅,以下均在root用户下操作,省去sudo ...
- C++标准库函数 end 的实现原理(非类型模板参数)
在刚开始学习<C++ Primer>的时候遇到了 end 函数,感觉很神奇,但又很迷惑:为什么能获得数组的尾后指针呢?编译器也不会在内存中申请一块空间放数组元素的个数啊!最近再一次遇到了 ...
- 一个最简单的通过自定义注解形式实现AOP的例子
1.首先实现AOP实例的第一步即声明切面类,两种方式(1.基于注解形式@Aspect,2.基于xml配置,一般都通过注解来声明切面类) 2.切入点表达式大致也有两种,一种是直接根据方法的签名来匹配各种 ...
- Flutter学习笔记(23)--多个子元素的布局Widget(Rwo、Column、Stack、IndexedStack、Table、Wrap)
如需转载,请注明出处:Flutter学习笔记(23)--多个子元素的布局Widget(Rwo.Column.Stack.IndexedStack.Table.Wrap) 上一篇梳理了拥有单个子元素布局 ...
- Vue入门到TodoList练手
学习资料 慕课网 - vue2.5入门 基础语法 示例代码1 <div id="root"> <h1>hello {{msg}}</h1> &l ...
- Java多线程之线程的启动
Java多线程之线程的启动 一.前言 启动线程的方法有如下两种. 利用Thread 类的子类的实例启动线程 利用Runnable 接口的实现类的实例启动线程 最后再介绍下java.util.concu ...
- java hdu A+B for Input-Output Practice (IV)
A+B for Input-Output Practice (IV) Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/327 ...
- 深入分析Mybatis 使用useGeneratedKeys获取自增主键
摘要 我们经常使用useGenerateKeys来返回自增主键,避免多一次查询.也会经常使用on duplicate key update,来进行insertOrUpdate,来避免先query 在i ...
- Error executing DDL via JDBC Statement 导致原因之一:列名使用了sql关键字
WARN: GenerationTarget encountered exception accepting command : Error executing DDL via JDBC Statem ...