怎么查看网站的ftp地址,前端开发人员招聘,网站网站设计,福建省城乡建设网站保姆级详解promise的核心功能#x1f4da;序言#x1f4cb;文章内容抢先看#x1f4f0;一、js的同步模式和异步模式1. 单线程#x1f4a1;2. 同步模式#x1f4a1;#xff08;1#xff09;定义#xff08;2#xff09;图例3. 异步模式#x1f4a1;#xff08;1… 保姆级详解promise的核心功能序言文章内容抢先看一、js的同步模式和异步模式1. 单线程2. 同步模式1定义2图例3. 异步模式1举例2定义3js如何实现异步4event loop过程4. 回调函数二、Promise异步方案1. Promise的三种状态1Promise的三种状态2状态解释2. 三种状态的变化和表现1状态的变化2状态的表现3. Promise的使用案例4. then和catch对状态的影响5. Promise的并行执行1Promise.all2Promise.race6. 两个有用的附加方法1done()2finally()三、实现Promise的核心功能1. 基础核心功能实现️1碎碎念2Promise基础功能的分析3Promise基础功能的实现4thenable功能的分析5thenable功能的实现2. 添加异步逻辑功能实现️1then中添加异步逻辑功能的分析2then中添加异步逻辑功能的实现3. 实现then方法的多次调用️1多次调用then方法的功能分析2多次调用then方法的功能实现4. 实现then方法的链式调用️1then方法链式调用的功能分析2then方法链式调用的功能实现3链式调用的自我检测5. promise的错误处理️1错误处理场景2错误处理功能实现6. 实现then方法的参数可选️1参数可选实现思路2参数可选功能实现7. 实现Promise.all️1Promise.all功能分析2Promise.all功能实现8. 实现Promise.resolve️1Promise.resolve功能分析2Promise.resolve功能实现四、结束语彩蛋 One More Thing课代表记录参考资料番外篇序言
众所周知 promise 是前端面试中雷打不动的面试题了面试官都很爱考。周一之前也是知识比较浮于表面在一些面经上看到了 promise 的实现方式就只停留在那个层面上。但实际上我发现如果没有深入其原理去理解面试官稍微变个法子来考这道题很容易就把我给问倒了。所以呀还是老老实实从头到尾研究一遍这样等遇到了不管怎么考万宗不变其一把原理理解了就没有那么容易被问倒了。
下面开始进入本文的讲解~️
文章内容抢先看 一、js的同步模式和异步模式
1. 单线程
大家都知道 js 的设计是基于单线程进行开发的它原先的目的在于只参与浏览器中DOM节点的操作。
而对于单线程来说其意味着只能执行一个任务且所有的任务都会按照队列的模式进行排队。
所以单线程的缺点就在于当 js 运行的时候 html 是不会进行渲染的。因此如果一个任务特别耗时那么将会很容易造成页面阻塞的局面。
为了解决这个问题 js 提出了同步模式和异步模式的解决方案。
2. 同步模式
1定义
所谓同步模式指的就是函数中的调用堆栈按照代码实现的顺序一步步进行。
2图例
接下来我们来用一段代码演示 js 中函数调用堆栈的执行情况。具体代码如下
const func1 () {func2();console.log(3);
}const func2 () {func3();console.log(4);
}const func3 () {console.log(5);
}func1(); //5 4 3看到这里相信很多小伙伴已经在构思其具体的执行顺序。下面用一张图来展示执行效果 对于栈这个数据结构来说它遵循后进先出的原则。因此当 func1 func2 func3 依次放进调用栈后 遵循后进先出原则 那么 func3 函数的内容会先被执行之后是 func2 最后是 func1 。
因此对于 js 的同步模式来说就是类似于上述的函数调用堆栈。
3. 异步模式
1举例
当程序遇到网络请求或定时任务等问题时这个时候会有一个等待时间。
假设一个定时器设置 10s 如果放在同步任务里同步任务会阻塞代码执行我们会等待 10s 后才能看到我们想要的结果。1个定时器的等待时间可能还好如果这个时候是100个定时器呢我们总不能等待着 1000s 的时间就为了看到我们想要的结果吧这几乎不太现实。
那么这个时候就需要异步通过异步来让程序不阻塞代码执行灵活执行程序。
2定义
对于同步模式来说它只能自上而下地一行一行执行一行一行进行解析。那与同步模式不同的是异步模式是按照我们想要的结果进行输出不会像同步模式一样产生阻塞以达到让程序可控的效果。
3js如何实现异步
相对于同步模式来说异步模式的结构更为复杂。除了调用栈之外 它还有消息队列和事件循环这两个额外的机制。所谓事件循环也称为 event loop 或事件轮询。因为 js 是单线程的且异步需要基于回调来实现所以 event loop 就是异步回调的实现原理。
JS在程序中的执行遵循以下规则
从前到后一行一行执行如果某一行执行报错则停止下面代码的执行先把同步代码执行完再执行异步。
一起来看一个实例
console.log(Hi);setTimeout(function cb1(){console.log(cb1); //cb1 即callback回调函数
}, 5000);console.log(Bye);//打印顺序
//Hi
//Bye
//cb1从上例代码中可以看到 JS 是先执行同步代码所以先打印 Hi 和 Bye 之后执行异步代码打印出 cb1 。
以此代码为例下面开始讲解 event loop 的过程。
4event loop过程
对于上面这段代码执行过程如下图所示 从上图中可以分析出这段代码的运行轨迹。首先 console.log(Hi) 是同步代码直接执行并打印出 Hi 。接下来继续执行定时器 setTimeout 定时器是异步代码所以这个时候浏览器会将它交给 Web APIs 来处理这件事情因此先把它放到 Web APIs 中之后继续执行 console.log(Bye) console.log(Bye) 是同步代码在调用堆栈 Call Stack 中执行打印出 Bye 。
到这里调用堆栈 Call Stack 里面的内容全部执行完毕当调用堆栈的内容为空时浏览器就会开始去 消息队列 Callback Queue 寻找下一个任务此时消息队列就会去 Web API 里面寻找任务遵循先进先出原则找到了定时器且定时器里面是回调函数 cb1 于是把回调函数 cb1 传入任务队列中此时 Web API 也空了任务队列里面的任务就会传入到调用堆栈里Call Stack 里执行最终打印出 cb1 。
4. 回调函数
早期我们在解决异步问题的时候基本上都是使用callback回调函数的形式 来调用的。形式如下
//获取第一份数据
$.get(url1, (data1) {console.log(data1);//获取第二份数据$.get(url2, (data2) {console.log(data2);//获取第三份数据$.get(url3, (data3) {console.log(data3);//还可以获取更多数据});});
});从上述代码中可以看到早期在调用数据的时候都是一层套一层 callback 调用 callback 仿佛深陷调用地狱一样数据也被调用的非常乱七八糟的。所以因为 callback 对开发如此不友好也就有了后来的 promise 产生。
promise 由 CommonJS 社区最早提出之后在2015年的时候 ES6 将其写进语言标准中统一了它的用法原生提供了 Promise 对象。 promise 的出现告别了回调地狱时代解决了回调地狱 callback hell 的问题。
那下面我们就来看看 Promise 的各种神奇用法~
二、Promise异步方案
1. Promise的三种状态
1Promise的三种状态
状态含义pending等待状态即在过程中还没有结果。比如正在网络请求或定时器没有到时间。fulfilled满足状态即事件已经解决了并且成功了当我们主动回调了 fulfilled 时就处于该状态并且会回调 then 函数。rejected拒绝状态即事件已经被拒绝了也就是失败了当我们主动回调了 reject 时就处于该状态并且会回调 catch 函数。
2状态解释
对于 Promise 来说它是一个对象用来表示一个异步任务在执行结束之后返回的结果它有 3 种状态 pending fulfilled rejected 。其执行流程如下 如果一个异步任务处于 pending 状态时那么表示这个 promise 中的异步函数还未执行完毕此时处于等待状态。相反如果 promise 中的异步函数执行完毕之后那么它只会走向两个结果
fulfilled 表示成功rejected 表示失败。
一旦最终状态从 pending 变化为 fulfilled 或者 rejected 后状态就再也不可逆。
所以总结来讲Promise 对象有以下两个特点
promise 对象的状态不受外界影响一旦状态被唤起之后函数就交由 web API 去处理这个时候在函数主体中再执行任何操作都是没有用的只会出现 pending → fulfilled或者 pending → rejected 状态即要么成功要么失败。即使再对 promise 对象添加回调函数也只会得到同样的结果即它的状态都不会再发生被改变。
2. 三种状态的变化和表现
1状态的变化
promise 主要有以上三种状态 pending 、 fulfilled 和 rejected 。当返回一个 pending 状态的 promise 时不会触发 then 和 catch 。当返回一个 fulfilled 状态时会触发 then 回调函数。当返回一个 rejected 状态时会触发 catch 回调函数。那在这几个状态之间他们是怎么变化的呢
1演示1
先来看一段代码
const p1 new Promise((resolved, rejected) {});console.log(p1, p1); //pending在以上的这段代码中控制台打印结果如下 在这段代码中 p1 函数里面没有内容可以执行所以一直在等待状态因此是 pending 。
2演示2
const p2 new Promise((resolved, rejected) {setTimeout(() {resolved();});
});console.log(p2, p2); //pending 一开始打印时
setTimeout(() console.log(p2-setTimeout, p2)); //fulfilled在以上的这段代码中控制台打印结果如下 在这段代码中 p2 一开始打印的是 pending 状态因为它没有执行到 setTimeout 里面。等到后续执行 setTimeout 时才会触发到 resolved 函数触发后返回一个 fulfilled 状态 promise 。
3演示3
const p3 new Promise((resolved, rejected) {setTimeout(() {rejected();});
});console.log(p3, p3);
setTimeout(() console.log(p3-setTimeout, p3)); //rejected在以上的这段代码中控制台打印结果如下。 在这段代码中 p3 一开始打印的是 pending 状态因为它没有执行到 setTimeout 里面。等到后续执行 setTimeout 时同样地会触发到 rejected 函数触发后返回一个 rejected 状态的 promise 。
看完 promise 状态的变化后相信大家对 promise 的三种状态分别在什么时候触发会有一定的了解。那么我们接下来继续看 promise 状态的表现。
2状态的表现
pending 状态不会触发 then 和 catch 。fulfilled 状态会触发后续的 then 回调函数。rejected 状态会触发后续的 catch 回调函数。
我们来演示一下。
1演示1
const p1 Promise.resolve(100); //fulfilled
console.log(p1, p1);
p1.then(data {console.log(data, data);
}).catch(err {console.error(err, err);
});在以上的这段代码中控制台打印结果如下 在这段代码中 p1 调用 promise 中的 resolved 回调函数此时执行时 p1 属于 fulfilled 状态 fulfilled 状态下只会触发 .then 回调函数不会触发 .catch 所以最终打印出 data 100 。
2演示2
const p2 Promise.reject(404); //rejected
console.log(p2, p2);
p2.then(data {console.log(data2, data);
}).catch(err {console.log(err2, err);
})在以上的这段代码中控制台打印结果如下 在这段代码中 p2 调用 promise 中的 reject 回调函数此时执行时 p1 属于 reject 状态 reject 状态下只会触发 .catch 回调函数不会触发 .then 所以最终打印出 err2 404 。
3. Promise的使用案例
对三种状态有了基础了解之后我们用一个案例来精进对 Promise 的使用。现在我们想要实现的功能是通过 fs 模块异步地调用本地的文件。如果文件存在那么在控制台上输出文件的内容如果文件不存在则将抛出异常。实现代码如下
const fs require(fs);const readFile (filename) {// 返回一个 promise 实例以供 then 调用const promise new Promise(function(resolve, reject){// 使用 readFile 去异步地读取文件异步调用也是 promise 函数的意义// 注意下面这个函数的逻辑是错误优先也就是先err再datafs.readFile(filename, (err, data) {// 如果文件读取失败就调取 reject 并抛出异常if(err){reject(err);}else{// 如果成功就调取 resolve 并返回调用成功的数据resolve(data);}});});return promise;
}// 测试代码
// 文件存在逻辑
const existedFile readFile(./test.txt);
existedFile.then((data) {// Buffer.from()方法用于创建包含指定字符串数组或缓冲区的新缓冲区。// Buffer.from(data).toString()读出文件里面的内容。文件里面记得写内容console.log(content: , Buffer.from(data).toString());},(error) {console.log(error);}
)// 文件不存在逻辑
const failFile readFile(./fail.txt);
failFile.then((data) {console.log(Buffer.from(data).toString());},(err) {console.log(err);}
);最终控制台的打印结果如下
[Error: ENOENT: no such file or directory, open C:\\promise\\fail.txt] {errno: -4058,code: ENOENT,syscall: open,path: C:\\promise\\fail.txt
}
content: 这是一个测试文件大家可以看到当 ./test.txt 文件存在时那么 existedFile 会去调用后续的 .then 回调函数因此最终返回调用成功的结果。注意这是一个测试文件 这行字就是 test 文件里面的内容。
同时 ./fail.txt 文件不存在因此 failFile 会调用后续的 .catch 文件同时将异常抛出。
现在大家应该对 promise 的使用有了一定的了解下面我们继续看 promise 中 then 和 catch 对状态的影响。
4. then和catch对状态的影响
then 正常返回 fulfilled 里面有报错则返回 rejected catch 正常返回 fulfilled 里面有报错则返回 rejected 。
我们先来看第一条规则 then 正常返回 fulfilled 里面有报错则返回 rejected 。
1演示1
const p1 Promise.resolve().then(() {return 100;
})
console.log(p1, p1); //fulfilled状态会触发后续的.then回调
p1.then(() {console.log(123);
});在以上的这段代码中控制台打印结果如下。 在这段代码中 p1 调用 promise 中的 resolve 回调函数此时执行时 p1 正常返回 fulfilled 不报错所以最终打印出 123 。
2演示2
const p2 Promise.resolve().then(() {throw new Error(then error);
});
console.log(p2, p2); //rejected状态触发后续.catch回调
p2.then(() {console.log(456);
}).catch(err {console.error(err404, err);
});在以上的这段代码中控制台打印结果如下。 在这段代码中 p2 调用 promise 中的 resolve 回调函数此时执行时 p2 在执行过程中抛出了一个 Error 所以里面有报错则返回 rejected 状态 所以最终打印出 err404 Error: then error 的结果。
我们再来看第二条规则 catch 正常返回 fulfilled 里面有报错则返回 rejected 。
1演示1需特别谨慎! !
const p3 Promise.reject(my error).catch(err {console.error(err);
});
console.log(p3, p3); //fulfilled状态注意触发后续.then回调
p3.then(() {console.log(100);
});在以上的这段代码中控制台打印结果如下。 在这段代码中 p3 调用 promise 中的 rejected 回调函数此时执行时 p3 在执行过程中正常返回了一个 Error 这个点需要特别谨慎这看起来似乎有点违背常理但对于 promise 来说不管时调用 resolved 还是 rejected 只要是正常返回而没有抛出异常都是返回 fulfilled 状态。所以最终 p3 的状态是 fulfilled 状态且因为是 fulfilled 状态之后还可以继续调用 .then 函数。
2演示2
const p4 Promise.reject(my error).catch(err {throw new Error(catch err);
});
console.log(p4, p4); //rejected状态触发.catch回调函数
p4.then(() {console.log(200);
}).catch(() {console.log(some err);
});在以上的这段代码中控制台打印结果如下。 在这段代码中 p4 依然调用 promise 中的 reject 回调函数此时执行时 p4 在执行过程中抛出了一个 Error 所以里面有报错则返回 rejected 状态 此时 p4 的状态为 rejected 之后触发后续的 .catch 回调函数。所以最终打印出 some err 的结果。
5. Promise的并行执行
1Promise.all
Promise.all 方法用于将多个 Promise 实例包装成一个新的 Promise 实例。比如
var p Promise.all([p1, p2, p3]);p的状态由 p1 、 p2 、 p3 决定分成两种情况
只有 p1 、 p2 、 p3 的状态都变为 fulfilled 最终 p 的状态才会变为 fulfilled 。此时 p1 、 p2 、 p3 的返回值组成一个数组并返回给 p 回调函数。只要 p1 、 p2 、 p3 这三个参数中有任何一个被 rejected 那么 p 的状态就会变成 rejected 。此时第一个被 rejected 的实例的返回值将会返回给 p 的回调函数。
下面用一个实例来展示 Promise.all 的使用方式。具体代码如下
//生成一个Promise对象的数组
var promises [4, 8, 16, 74, 25].map(function (id) {return getJSON(/post/ id .json);
});Promise.all(promises).then(fucntion (posts) {// ...
}).catch(function (reason) {// ...
}}大家可以看到对于以上代码来说 promises 是包含5个Promise实例的数组只有这5个实例的状态都变成 fulfilled 或者其中有一个变为 rejected 那么才会调用 Promise.all 方法后面的回调函数。 这里有一种值得注意的特殊情况是如果作为参数的 Promise 实例自身定义了 catch 方法那么它被 rejected 时并不会触发 Promise.all() 的 catch 方法。这样说可能比较抽象我们用一个实例来展示一下具体代码如下
const p1 new Promise((resolve, reject) {resolve(hello);
}).then(result {return result;
}).catch(e {return e;
});const p2 new Promise((resolve, reject) {throw new Error(报错了);
}).then(result {return result;
}).catch(e {return e;
});Promise.all([p1, p2]).then(result {console.log(result);
}).catch(e {console.log(e);
})在上面的代码中 p1 会 resolve 之后调用后续的 .then 回调函数。而 p2 会 reject 因此之后会调用后续的 .catch 回调函数。注意这里的 p2 有自己的 catch 方法且该方法返回的时一个新的 Promise 实例而 p2 实际上指向的就是这个实例。
所以呢这个实例执行完 catch 方法后也会变成 resolved 。因此 在 Promise.all() 这个方法中其参数里面的两个实例就都会 resolved 所以之后会调用 then 方法指定的回调函数而不会调用 catch 方法指定的回调函数。
2Promise.race
Promise.race 方法同样是将多个 Promise 实例包装成一个新的 Promise 实例。比如
var p Promise.race([p1, p2, p3]);我们同样用以上这段代码来进行分析。与 Promise.all() 不同的是只要 p1 、 p2 、 p3 中有一个实例率先改变状态那么** p 的状态就会跟着改变**且那个率先改变的 Promise 实例的返回值就会传递给 p 的回调函数。
所以呀为什么它叫 race race 顾名思义就是竞赛的意思。在赛场上第一名永远只有一个。而我们可以把第一名视为第一个 resolve 状态的 promise 只要第一名出现了那么结果就是第一名赢了所以返回的值就是第一个为 resolve 的值。其他人再怎么赛跑都逃不过拿不到第一的现实。
6. 两个有用的附加方法
ES6 中 Promise API 并没有提供很多方法但是我们可以自己来部署一些有用的方法。接下来我们将来部署两个不在 ES6 中但是却很有用的方法。
1done()
无论 Promise 对象的回调链以 then 方法还是 catch 方法结尾只要最后一个方法抛出错误那么都有可能出现无法捕捉到的情况。这是为什么呢原因在于 promise 内部的错误并不会冒泡到全局。因此我们提供了一个 done 方法。done 方法总是处于回调链的尾端保证抛出任何可能出现的错误。我们来看下它的使用方式具体代码如下
asyncFunc().then(f1).catch(r1).then(f2).done();同时呢它的实现代码也比较简单我们来看一下。具体代码如下
Promise.prototype.done function (onFulfilled, onRejected) {this.then(onFulfilled, onRejected) .catch(function (reason) {//抛出一个全局错误setTimeout(() {throw reason;}, 0);})
}由以上代码可知 done 方法可以像 then 方法那样使用提供 fulfilled 和 rejected 状态的回调函数也可以不提供任何参数。但是不管如何 done 方法都会捕捉到任何可能出现的错误并向全局抛出。
2finally()
finally 方法用于指定不管 Promise 对象最后状态如何都会执行的操作。它与 done 方法最大的区别在于它接受一个普通的回调函数作为参数该函数不管怎样都必须执行。
下面来展示一个例子。假设我们现在有一台服务器现在让这台服务器使用 Promise 来处理请求然后使用 finally 方法关掉服务器。具体实现代码如下
server.listen(0).then(function () {// run test
}).finally(server.stop);同样地它的实现代码也比较简单我们来看一下。具体代码如下
Promise.prototype.finally function (callback) {let p this.constructor;return this.then(value p.resolve(callback()).then(() {return value;}),reason p.resolve(callback()).then(() {throw reason;}));
};通过以上代码我们可以了解到不管前面的 promise 是 fulfilled 还是 rejected 最终都会执行回调函数 callback 。
三、实现Promise的核心功能
1. 基础核心功能实现️
1碎碎念
接下来我们先来实现 promise 最基础的核心功能也就是 promise.resolve() 和 promise.reject() 这两个函数。
注意基础功能除了构造器 constructor 意以外其余的实现都不是绑定在原型链上的函数将会使用箭头函数来进行实现。
2Promise基础功能的分析
我们先来看下 promise 的基本使用是怎么样的具体代码如下
/*** 01_promise的基本使用*/
const promise new Promise(function(resolve, reject) {if (success) {resolve(value);} else {reject(error);}
});根据使用方式我们可以得出 promise 有以下几个特点
promise 是一个对象当我们新建一个 promise 对象的同时需要传进去一个回调函数这个回调函数又需要接收两个回调函数 resolve 和 reject 且用这两个回调函数来作为参数之后呢 当调用成功时使用 resolve 回调函数而当调用失败时使用 reject 回调函数。resolve 和 reject 这两个回调函数都将会被用来修改 promise 的状态 resolve 会把 pending 状态修改为 fulfilled 而 reject 将会把 pending 状态修改为 rejected 。同时值得注意的是一旦状态确定后后续所有操作的状态将不会再被更改即不可逆。
3Promise基础功能的实现
我们现在来实现 promise 的基本功能该功能含有以下几个组成要素
实现 PromiseMon 的基础结构其中包含构造函数和状态实现 resolve 和 reject 功能这里先实现状态从 pending 到 fulfilled 或 rejected 的改变其余状态间的改变暂未实现。
具体实现代码如下
/*** 02_promise基础功能的实现*/// 定义pending、fulfilled和rejected三个常量
const PENDING pending;
const FULFILLED fulfilled;
const REJECTED rejected;class PromiseMon {// 定义Promise中的状态默认状态为pendingstatus PENDING;// cb即callback,是传给promise的回调函数constructor(cb) {// cb这个回调函数会被立即执行作用是判断状态是否应该进行更改cb(this.resolve, this.reject);}// 使用箭头函数的原因箭头函数可以减少this指向造成的问题将其绑定在promise的实例对象// resolve回调函数resolve () {// 只有当状态为pending时才能修改if(this.status ! PENDING) {return;}else{this.status FULFILLED;}};// reject回调函数reject () {只有当状态为pending时才能修改if(this.status ! PENDING) {return;}else{this.status REJECTED;}}}// 调用resolve和reject来验证状态在确定之后不可逆
const promise1 new PromiseMon((resolve, reject) {resolve(resolved);reject(rejected);
});const promise2 new PromiseMon((resolve, reject) {reject(rejected);resolve(resolved);
});console.log(promise1.status); // fulfilled
console.log(promise2.status); // rejected4thenable功能的分析
上面我们简单封装了 PromiseMon 这个函数那现在呢我们继续用它来实现 thenable 的功能。
大家都知道 promise 在调用了 resolve 和 reject 方法之后就该来触发后续的 .then 或者 .catch 方法了。如果没有这两个方法的话那么 promise 返回的数据都没啥使用的地儿那还返回这个数据来干嘛对吧。
我们现在先来看关于 then 的基本使用操作。具体代码如下
const fs require(fs);const readFile (filename) {const promise new Promise(function(resolve, reject){fs.readFile(filename, (err, data) {if(err){reject(err);}else{resolve(data);}});});return promise;
}const existedFile readFile(./test.txt);
existedFile.then((data) {console.log(content: , Buffer.from(data).toString());},(error) {console.log(error);}
)综上代码我们来分析 then 函数的几个特点
then 函数接收两个参数第一个参数在异步操作成功时进行调用第二个则是在操作失败时调用。then 函数需要能够分析 promise 的状态分析完 promise 的状态后再决定去调用成功或失败的回调函数。then 方法被定义在原型对象上 Promise.prototype.then() 。then 调用成功的回调函数时会接收一个成功的数据作为参数同样地当他调用失败的回调函数时在此之前它也会接收到一个失败的原因来作为参数进行传递。then 会先接收到一个成功状态的数据那么这个数据就用来作为参数这个参数供给处理成功状态的回调函数进行调用同样地当处理失败状态时 then 会先接收到一个失败状态的数据之后这个数据用来作为参数这个参数供给处理失败状态的回调函数进行调用。说的这么绕总结一下就是接收当前状态数据→数据作为参数→拿来给回调函数调用。
5thenable功能的实现
我们现在来实现 thenable 的基本功能该功能含有以下几个组成要素
将在原型上实现 then 方法修改原先的 resolve 函数实现对成功状态的数据进行绑定修改原先的 reject 函数实现对失败状态的原因进行绑定。
具体实现代码如下
/*** 03_thenable功能的实现*/const PENDING pending;
const FULFILLED fulfilled;
const REJECTED rejected;class PromiseMon {status PENDING;// 定义成功和失败时的值默认都是未定义value undefined;reason undefined;constructor(cb) {// cb这个回调函数会被立即执行作用是判断状态是否应该进行更改cb(this.resolve, this.reject);}// 修改参数让resolve接收成功后传来的值resolve (value) {if(this.status ! PENDING) {return;}else{this.status FULFILLED;// 将成功的值赋予给valuethis.value value;}};// 修改参数让reject接收失败后传来的原因reject (reason) {if(this.status ! PENDING) {return;}else{this.status REJECTED;// 将失败的原因赋予给reasonthis.reason reason;}}/** then要接收两个回调函数successCB这个回调函数在状态为fulfilled时使用* 而failCB这个回调函数在状态为rejected时进行使用*/ then(successCB, failCB) {if(this.status FULFILLED) {successCB(this.value);}else if(this.status REJECTED) {failCB(this.reason);}}}// 测试用例
//成功状态测试
const successPromise new PromiseMon((resolve, reject) {resolve(successData);reject(failData);
});console.log(成功状态, successPromise.status); // 成功状态successDatasuccessPromise.then((value) {console.log(success:, value); // success:successData},(reason) {console.log(error:, reason); // 没有输出}
)//失败状态测试
const failPromise new PromiseMon((resolve, reject) {reject(failData);resolve(successData);
});console.log(失败状态, failPromise.status); // 失败状态failDatafailPromise.then((value) {console.log(success:, value); // 没有输出},(reason) {console.log(error:, reason); // error:failData}
)到这里我们就实现了一个最基础的、且同步执行的 Promise 。接下来我们来为这个同步的 Promise 添加异步逻辑。
2. 添加异步逻辑功能实现️
1then中添加异步逻辑功能的分析
一般来说我们在 promise 中被调用的大部分都是异步函数比如 setTimeout 、 setInterval 等等。所以呢我们现在要在 then 中添加异步的功能来实现对异步逻辑进行操作。
在上面的 then 方法中大家定位到 if……else if…… 部分上面所写的逻辑只有对状态为 fulfilled 和 rejected 时才进行判断而没有对状态为 pending 时进行判断。
所以当状态为 pending 时意味着 PromiseMon 中的异步函数还没有执行完毕。这个时候我们需要将 succesCB 和 failCB 这两个回调函数给先存到一个变量中去等到后续异步内容结束后再进行调用。
2then中添加异步逻辑功能的实现
依据上面的分析我们来实现这个异步功能。具体代码如下
/*** 04_添加异步逻辑功能的实现*/const PENDING pending;
const FULFILLED fulfilled;
const REJECTED rejected;class PromiseMon {status PENDING;value undefined;reason undefined;// 定义两个变量来存放成功和失败时的回调函数successCB undefined;failCB undefined;constructor(cb) {cb(this.resolve, this.reject);}resolve (value) {if(this.status ! PENDING) {return;}else{this.status FULFILLED;this.value value;/*** 表达式a 表达式b* 计算表达式a的运算结果* 如果为true执行表达式b并返回b的结果* 如果为false返回a的结果。*//*** 当successCB里面有存放成功的回调函数时则表明this.successCB为true* 继续判断新传来的值的状态是否为成功状态的值* 如果是则将新的值传给this.successCB回调函数* 如果否则返回原来存放着的this.success的结果*/ this.successCB this.successCB(this.value);}};reject (reason) {if(this.status ! PENDING) {return;}else{this.status REJECTED;this.reason reason;// 存放调用失败的回调函数this.failCB this.failCB(this.reason);}}/** then要接收两个回调函数successCB这个回调函数在状态为fulfilled时使用* 而failCB这个回调函数在状态为rejected时进行使用*/ then(successCB, failCB) {if(this.status FULFILLED) {successCB(this.value);}else if(this.status REJECTED) {failCB(this.reason);}// 当函数还没有执行完毕时只能等待else{ // 将两个回调函数的值存放起来this.successCB successCB;this.failCB failCB;}}}// 测试用例
// 测试异步成功状态
const asyncPromise1 new PromiseMon((resolve, reject) {setTimeout(() {resolve(asyncSuccessData);}, 2000);
});asyncPromise1.then((value) {console.log(异步成功状态, value);},(reason) {console.log(异步失败状态:, reason);}
);// 测试异步失败状态
const asyncPromise2 new PromiseMon((resolve, reject) {setTimeout(() {reject(asyncErrorData);}, 1000);
});asyncPromise2.then((value) {console.log(异步成功状态, value);},(reason) {console.log(异步失败状态:, reason);}
);/*** 打印结果* 异步失败状态: asyncErrorData* 异步成功状态 asyncSuccessData*/到这里异步的功能我们也就实现啦但是上面的 then 我们还只是实现 then 方法的一次调用接下来我们来实现 then 方法的多次调用。
3. 实现then方法的多次调用️
1多次调用then方法的功能分析
多次调用 then 方法分为两种情况
同步调用 then 方法。同步调用 then 方法相对比较简单只要直接调用 successCB 或 failCB 回调函数即可。异步调用 then 方法。之前的属性 successCB 和 failCB 两个回调函数是存放为对象形式因此我们需要先优化我们的存储形式。优化完成之后将所有的回调函数全部存放在一起等到执行完毕之后再依次调用。
2多次调用then方法的功能实现
依据上述的分析我们来实现多次调用 then 方法的功能。具体代码如下
/*** 05_多次调用then方法功能的实现*/const PENDING pending;const FULFILLED fulfilled;const REJECTED rejected;class PromiseMon {status PENDING;value undefined;reason undefined;// 定义两个数组变量来各自存放成功和失败时的所有回调函数successCB [];failCB [];constructor(cb) {cb(this.resolve, this.reject);}resolve (value) {if(this.status ! PENDING) {return;}else{this.status FULFILLED;this.value value;// 使用 shift()方法来弹出并返回第一个元素while(this.successCB.length){this.successCB.shift()(this.value);}}};reject (reason) {if(this.status ! PENDING) {return;}else{this.status REJECTED;this.reason reason;// 使用 shift()方法来弹出并返回第一个元素while(this.failCB.length){this.failCB.shift()(this.reason);}}}then(successCB, failCB) {if(this.status FULFILLED) {successCB(this.value);}else if(this.status REJECTED) {failCB(this.reason);}else{// 通过push方法将回调函数的值存放到数组中this.successCB.push(successCB);this.failCB.push(failCB);}}}// 测试用例
const multiplePromise1 new PromiseMon((resolve, reject) {setTimeout(() {resolve(multiSuccessData);}, 2000);
});
multiplePromise1.then((value) {console.log(第一次调用成功, value); // 第一次调用成功 multiSuccessData
});
multiplePromise1.then((value) {console.log(第二次调用成功, value); // 第二次调用成功 multiSuccessData
});/*** 打印结果* 第一次调用成功 multiSuccessData* 第二次调用成功 multiSuccessData*/讲到这里关于对此调用 then 方法的功能就实现完成了。现在我们继续来实现关于 then 方法的链式调用。
4. 实现then方法的链式调用️
1then方法链式调用的功能分析
我们先来对 then 方法的链式调用进行功能分析具体如下
不考虑其他功能的前提下先完成链式调用的嵌套实现链式调用的大前提是每一个 then 函数返回的都必须是一个 Promise 对象否则就无法衔接地去使用 then 函数。因此首先我们需要在 then 函数中新建一个 Promise 对象之后呢在新建的 promise 对象里面去处理内部使用的 resolve 和 reject 所返回的值最终 then 函数也就返回了 promise 对象。
2then方法链式调用的功能实现
依据上面的功能分析我们来实现 then 的链式调用功能。具体代码如下
/*** 06_then的链式调用功能的实现*/const PENDING pending;const FULFILLED fulfilled;const REJECTED rejected;class PromiseMon {status PENDING;value undefined;reason undefined;// 定义两个数组变量来各自存放成功和失败时的所有回调函数successCB [];failCB [];constructor(cb) {cb(this.resolve, this.reject);}resolve (value) {if(this.status ! PENDING) {return;}else{this.status FULFILLED;this.value value;while(this.successCB.length){this.successCB.shift()(this.value);}}};reject (reason) {if(this.status ! PENDING) {return;}else{this.status REJECTED;this.reason reason;while(this.failCB.length){this.failCB.shift()(this.reason);}}}then(successCB, failCB) {// /*** 新建一个promise对象来给下一个then使用。* 三种状态* ①promise对象执行成功调用resolve* ②promise对象执行失败则调用reject* ③promise对象还未执行将回调函数推入准备好的数组中。*/const thenablePromise new PromiseMon((resolve, reject) {if(this.status FULFILLED) {const thenableValue successCB(this.value);// 判断返回的值是否是promise对象resolvePromise(thenableValue, resolve, reject);}else if(this.status REJECTED) {const thenableReason failCB(this.reason);// 判断返回的值是否是promise对象resolvePromise(thenableReason, resolve, reject);}else{// 通过箭头函数的方式将回调函数的值存放进数组中this.successCB.push(() {const thenableValue successCB(this.value);resolvePromise(thenableValue, resolve, reject);});this.failCB.push(() {const thenableReason failCB(this.reason);resolvePromise(thenableReason, resolve, reject);});}});return thenablePromise;}
}/*** 判断传进来的thenablePromise是否是promise对象* 如果是则调用then函数来处理如果否则直接返回值。*/
const resolvePromise (thenablePromise, resolve, reject) {// 判断是否是一个promise对象if(thenablePromise instanceof PromiseMon) {thenablePromise.then(resolve, reject);} else {// 如果不是promise对象则直接返回值resolve(thenablePromise);}
}// 测试用例
// 测试链式调用成功状态
const thenablePromise new PromiseMon((resolve, reject) {resolve(thenableSuccessData);
});const otherPromise () {return new PromiseMon((resolve, reject) {setTimeout(() {resolve(otherPromise);}, 2000);});
}const anotherPromise () {return new PromiseMon((resolve, reject) {setTimeout(() {resolve(anotherPromise);});});
}thenablePromise.then((value) {console.log(第一次链式调用成功, value, new Date()); // 第一次调用成功 thenableSuccessData// return的结果是为了给下一个then使用return otherPromise();}).then((value) {console.log(第二次链式调用成功, value, new Date()); // 第二次调用成功 otherPromisereturn anotherPromise();}).then((value) {console.log(第三次链式调用成功, value, new Date()); // 第三次调用成功 anotherPromise})/*** 打印结果* 第一次链式调用成功 thenableSuccessData 2021-08-04T11:13:25.868Z* 第二次链式调用成功 otherPromise 2021-08-04T11:13:25.877Z* 第三次链式调用成功 anotherPromise 2021-08-04T11:13:25.878Z*/至此我们就完成了 promise 的链式调用。
3链式调用的自我检测
有时候我们有可能在调用 promise 时会陷入自我调用的境地。也就是无限的循环嵌套和无限的自我调用。比如下面这种情况
const promise1 new Promise((resolve, reject) {resolve(success);
});// 无限循环嵌套无限自我调用
// 当运行时控制台会抛出异常
const promise2 promise1.then((val) {return promise2;
});// 打印结果
// TypeError: Chaining cycle detected for promise #Promise因此现在我们要做的是在 resolvePromise 函数中新增一个参数这个参数就是当前所创造的 promise 对象。之后判断两个 promise 对象是否相等如果相等那么就抛出异常。依据这个逻辑我们来修改上面链式调用的功能代码达到禁止自我调用的闭环。具体代码如下
/*** 07_链式调用的自我检测*/const PENDING pending;
const FULFILLED fulfilled;
const REJECTED rejected;class PromiseMon {status PENDING;value undefined;reason undefined;successCB [];failCB [];//此处省略constructor代码//此处省略resolve代码//此处省略reject代码then(successCB, failCB) {const thenablePromise new PromiseMon((resolve, reject) {/*** 利用setTimeout是异步函数的特性* 这样setTimeout里面的内容会在同步函数执行完之后才会进行* 所以当resolvePromise在执行的时候thenablePromise就已经被实例化了* 使得resolvePromise顺利的调用thenablePromise*/if(this.status FULFILLED) {setTimeout(() {const thenableValue successCB(this.value);resolvePromise(thenablePromise, thenableValue, resolve, reject);}, 0);}else if(this.status REJECTED) {setTimeout(() {const thenableReason failCB(this.reason);resolvePromise(thenablePromise, thenableReason, resolve, reject);}, 0);}else {this.successCB.push(() {setTimeout(() {const thenableValue successCB(this.value);resolvePromise(thenablePromise, thenableValue, resolve, reject);}, 0);});this.failCB.push(() {setTimeout(() {const thenableReason failCB(this.reason);resolvePromise(thenablePromise, thenableReason, resolve, reject);}, 0);});}});return thenablePromise;}const resolvePromise (createPromise, thenablePromise, resolve, reject) {if(createPromise thenablePromise) {return reject(new TypeError(出现循环调用的情况 Chaning cycle detected))}else if(thenablePromise instanceof PromiseMon) {thenablePromise.then(resolve, reject);} else {resolve(thenablePromise);}
}// 测试用例
// 测试自我调用
const thenablePromise new PromiseMon((resolve, reject) {resolve(chainningData);
});const chainingPromise thenablePromise.then((value) {console.log(数据调用成功, value, new Date());return chainingPromise;
})
//会报错出现循环调用
chainingPromise.then((value) {console.log(执行操作成功, value, new Date());},(reason) {console.log(执行操作失败, reason, new Date());}
);/*
打印结果
数据调用成功 chainningData 2021-08-04T11:28:39.984Z
执行操作失败 TypeError: 出现循环调用的情况 Chaning cycle detected
*/至此我们完成了链式调用的自我检测。
5. promise的错误处理️
1错误处理场景
到这里我们对 pormise 的 then 方法基本实现的差不多。但是还有一个很重要但是又很容易被我们疏忽的问题就是错误处理。现在我们来分析一下可能会出现异常的常见场景
构造器中的回调函数 cb 需进行 try/catch 处理then 函数中的错误处理需要对同步函数和异步函数进行 try/catch 处理。
2错误处理功能实现
依据上面的场景分析我们来实现 promise 的错误处理功能。具体代码如下
/*** 08_promise的错误处理*/const PENDING pending;
const FULFILLED fulfilled;
const REJECTED rejected;class PromiseMon {status PENDING;value undefined;reason undefined;successCB [];failCB [];constructor(cb) {try {cb(this.resolve, this.reject);} catch (err) {this.reject(err in cb);}}//此处省略resolve代码//此处省略reject代码then(successCB, failCB) {const thenablePromise new PromiseMon((resolve, reject) {// 给setTimeout这个异步函数添加try/catchif(this.status FULFILLED) {setTimeout(() {try {const thenableValue successCB(this.value);resolvePromise(thenablePromise, thenableValue, resolve, reject);} catch (err) {reject(err);}}, 0);}else if(this.status REJECTED) {setTimeout(() {try {const thenableReason failCB(this.reason);resolvePromise(thenablePromise, thenableReason, resolve, reject);} catch (err) {reject(err);}}, 0);}else {// resolvePromise 同时要加到successCB和failCB中进行处理this.successCB.push(() {setTimeout(() {try {const thenableValue successCB(this.value);resolvePromise(thenablePromise, thenableValue, resolve, reject);} catch (err) {reject(err);}}, 0);});this.failCB.push(() {setTimeout(() {try {const thenableReason failCB(this.reason);resolvePromise(thenablePromise, thenableReason, resolve, reject);} catch (err) {reject(err);}}, 0);});}});return thenablePromise;}// 此处省略resolvePromise代码// 测试用例
const promise new PromiseMon((resolve, reject) {setTimeout(() {resolve(successData);});
});promise.then((value) {console.log(value); // (1) 打印 successDatathrow new Error(error); // (2) 抛出异常},(reason) {console.log(reason);return fail in then;}).then((value) {console.log(value);},(reason) {console.log(reason);return callback fail; // 抛出异常后返回 callback fail 给下面的then方面调用}).then((value) {console.log(value);},(reason) {console.log(reason); // (3)上面传来callback fail因此打印 callback fail});/*
打印结果
successData
Error: error
callback fail
*/大家可以看到通过 try/catch 的方式对遇到的错误进行处理并且最终抛出异常以及返回 reason 的值。
6. 实现then方法的参数可选️
1参数可选实现思路
大家可以发现上面我们在调用 then 方法的时候一直都是需要进行参数传递的这样看起来好像还不是特别友好。因此呢我们现在来实现这个功能让 then 方法的参数可以有传或者不传这 2 种操作。实现思路也比较简单就是在 then 函数中判断是否传入参数如果没有的话则返回原来的 value 就好了。类似于下面这种形式
promise.then((val) val) // 这样使用箭头函数表明直接返回 value.then((val) val) .then((val) val) .then((val) {console.log(val); // 200});2参数可选功能实现
依据上面的是实现思路接下来我们来实现这个功能。具体代码如下
下面我们对 then 函数进行改造
then(successCB, failCB) {successCB successCB ? successCB : (value) value;failCB failCB ? failCB : (reason) { throw reason;};
}来用两组测试用例进行测试
// 测试用例
// 成功状态下的用例const successpromise new PromiseMon((resolve, reject) {resolve(100);});successpromise.then().then().then().then((val) {console.log(val); // 100});// 失败状态下的用例
const failPromise new PromiseMon((resolve, reject) {reject(200);
});failPromise.then().then().then().then((val) {},(reason) {console.log(reason); // 200});
/*** 打印结果* 100* 200
*/大家可以看到不管是 resolve 还是 reject 状态都一一完成了对参数可选功能的实现。
7. 实现Promise.all️
1Promise.all功能分析
上面在讲 Promise 异步方案的时候就已经讲过 promise.all 和 promise.race 方法。现在我们来梳理下实现思路
Promise.all 是一个静态方法它接收一个 Promise 数组 作为参数。Promise.all 的特点在于它可以按照顺序去获取所有调用的异步函数。js 的关键字 static 将会把对应的变量或函数绑定到class类上而不是绑定在 ** prototype 原型**上。
2Promise.all功能实现
依据实现的这个逻辑来实现 promise.all 这个功能。具体代码如下
/*** 10_promise.all功能的实现*/const PENDING pending;
const FULFILLED fulfilled;
const REJECTED rejected;class PromiseMon {status PENDING;value undefined;reason undefined;successCB [];failCB [];//省略constructor、resolve、reject和then方法的代码static all(arr){const results [];let index 0;return new PromiseMon((resolve, reject) {// 添加数据的逻辑把指定的数据添加到数组的对应位置上const addData (idx, val) {results[idx] val;index;// 进行这一步判断的目的为了等待异步操作完成if(index arr.length) {resolve(results);}}// 对数组进行循环获取所有的数据arr.forEach((cur, index) {// 如果传进来的值是一个promise对象if(cur instanceof PromiseMon){cur.then((value) addData(index, value),(reason) reject(reason))}// 如果传进来的是普通值,而非promise对象else {addData(index, cur);}});});}
}// 此处省略resolvePromise代码// 测试用例
const promise () {return new PromiseMon((resolve, reject) {resolve(100);});
}const promise2 () {return new PromiseMon((resolve, reject) {setTimeout(() {resolve(200);}, 1000);});
}//因为使用static关键字所以可以直接在类上面进行调用
PromiseMon.all([a, b, promise(), promise2(), c]).then((res) {console.log(res); // [ a, b, 100, 200, c ]
})
大家可以看到即使 promise2 是异步函数但最终也正常的显示在数组当中且按序的一一进行打印。到此也就说明 promise.all 成功实现啦
同时 promise.race 也是按照这个模式去实现这里不再进行讲解~
8. 实现Promise.resolve️
1Promise.resolve功能分析
我们先来梳理下 promise.resolve 的实现思路
如果参数是一个 promise 实例那么 promise.resolve() 将不做任何修改原封不动地返回这个实例。参数不是 promise 实例或根本不是一个对象则 Promise.resolve() 方法返回一个新的 promise 对象此时状态为 fulfilled 。不带有任何参数则直接返回一个 fulfilled 状态的 promise 对象。
2Promise.resolve功能实现
依据以上的功能分析来实现 promise.resolve 这个功能。具体代码如下
/*** 11_promise.resolve功能的实现*/const PENDING pending;
const FULFILLED fulfilled;
const REJECTED rejected;class PromiseMon {status PENDING;value undefined;reason undefined;successCB [];failCB [];//省略constructor、resolve、reject和then方法的代码static resolve(value){// 传进来的是一个promise对象原封不动返回if(value instanceof PromiseMon) {return value;} // 传进来的不是promise对象将其作为参数返回一个promiseelse {return new PromiseMon((resolve, reject) {resolve(value);})}}
}// 此处省略resolvePromise代码// 测试用例
const promise () {return new PromiseMon((resolve, reject) {resolve(100);});
}// 1.参数是一个promise实例那么不做任何修改原封不动地返回这个实例
PromiseMon.resolve(promise).then((res) {console.log(res); // 100
})/*** 2.参数不是具有then方法的对象或根本就不是对象* 则Promise.resolve()方法返回一个新的promise对象状态为fulfilled*/
PromiseMon.resolve(200).then((res) {console.log(res); // 200
})/*** 3.不带有任何参数则直接返回一个resolved状态的promise对象*/
PromiseMon.resolve().then(function () {console.log(two); // two
});大家可以看到依据我们所罗列的三种情况 promise.resolve 的功能也一一实现啦
同时 promise.reject 也是按照这个模式去实现这里不再进行讲解~
四、结束语
写到这里的时候发现我已经花了整整三天的时间大约接近34h在 promise 这个知识上好在最终算是对 promise 核心功能的的实现有一个较为满意的结果。
可能也是第一次这么细致的去啃一个知识所以在学习过程中遇到很多以前没踩过的坑中间过程中对问题进行详细记录并尝试解决慢慢的就完善了一个新的知识体系。
最后本文讲解到这里就结束啦希望大家能对 promise 有一个更好的了解~
彩蛋 One More Thing
课代表记录
✅《三 7.》的 Promise.race 方法未用代码在原文中实现。
✅《三 8.》中据资料调查 promise.resolve 还有第4种传参方式当参数是一个 thenable 即参数是一个带有 then 方法的对象时则结果返回具有 then 方法的对象本功能暂未实现。
✅《三 8.》的 Promise.reject 方法未用代码在原文中实现。
参考资料 [万字详解]JavaScript 中的异步模式及 Promise 使用 [1w6k 字详细讲解] 保姆级一步一步带你实现 Promise 的核心功能 ES6快速入门
番外篇 关注公众号星期一研究室第一时间关注优质文章更多精选专栏待你解锁~如果这篇文章对你有用记得留个脚印jio再走哦~以上就是本文的全部内容我们下期见