【JS】强化Promise理解,从零手写属于自己的Promise.all与Promise.race

壹 ❀ 引
在一个思路搞定三道Promise并发编程题,手摸手教你实现一个Promise限制器一文中,我们在文章结尾留了一个疑问,关于第三题的实现能否解决当每次调用时间都不相等的情况(比如第二次调用要早于第一次调用结束),那么最终得到的结果顺序还能与参数顺序保持一致问题?在分享我踩坑过程中其实已经证明是可以满足这种场景的,但为什么呢?
我们可以尝试运行下面代码,你会发现尽管输出顺序不对,但每次index
与value
都是正确的配队关系:
const time = [1, 3, 4, 2, 1];
// 假设请求API为
function request(params) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(params), time[Math.floor(Math.random() * 5)] * 1000);
});
}
// 最多处理3个请求的调度器
function Scheduler(list = [], limit = 3) {
let count = 0;
// 用于统计成功的次数
let resLength = 0;
// 浅拷贝一份,原数据的length我们还有用
const pending = [...list];
const resList = [];
// 一定得返回一个promise
return new Promise((resolve, reject) => {
const run = () => {
if (!pending.length || count >= limit) return;
count++;
const index = list.length - pending.length;
const params = pending.shift();
request(params)
.then((res) => {
console.log('当前index为:', index, '当前结果为:', res);
count--;
resLength++;
// 按index来保存结果
resList[index] = res;
// 全部成功了吗?没有就继续请求,否则resolve(resList)跳出递归;
resLength === list.length ? resolve(resList) : run();
})
.catch(reject) // 有一个失败就直接失败
};
// 遍历,模拟前两次依次调用的动作,然后在run内部控制如何执行
list.forEach(() => run());
})
}
Scheduler([1, 2, 3, 4, 5]).then((res) => console.log('最终结果为:', res)); // 1 2 3 4 5

可以毫不夸张的说,上述代码已经算是一个满足了并发限制器功能的Promise.all
了,只要我们去除掉限制部分代码,稍加修改就能分别得到Promise.all
与Promise.race
的实现,但在改写之前我们还是先解释为什么尽管执行顺序不同,为什么结果与参数仍是对应关系的问题,而且我觉得也只剩下这一个稍微有点饶的疑惑点了。
贰 ❀ 执行上下文与闭包
还是模拟下上述代码的执行过程,当forEach
遍历调用run
时,可以确定的是,如下代码绝对是同步执行完成的,且5次都是同步跑完:
// 获得当前的index
const index = list.length - pending.length;
// 获取当前请求需要的参数
const params = pending.shift();
异步的是request()
,你什么时候能执行我不关系,反正一开始我已经把你执行需要的参数成对的给你准备好了。有同学的疑问可能就在于,我也知道这些参数一开始是成对的,那Promise
执行顺序被打乱之后,后执行的Promise
又怎么知道之前的index
是多少呢,这是怎么对应上的?
问题又回到了老生常谈的执行上下文与闭包问题。我们知道代码在执行前都要经历执行上下文创建阶段与执行阶段,而一个函数的执行上下文在它创建时就已经决定了,而不是执行时,这也是典型的静态作用域的概念,比如:
const a = 1;
const fn = () => {
console.log(a);
};
(() => {
const a = 2;
fn();// ???
})();
以上代码fn
执行时输出1,这是因为fn
的执行上下文在创建时决定,而不是执行时,所以不管你在哪调用它,它能访问的永远是同出一个作用域下的const a = 1
,这里就当简单复习下静态作用域的概念。
回到上文我们实现的代码,我们知道request().then()
这个调用行为是同步的,异步的是requset
内部修改状态的代码,以及状态修改完成后才能执行的.then()
所注册的回调函数,注意.then()
注册回调的行为是同步的,这一点你一定要搞清楚。
也就是说,在五次同步的run()
调用过程中,index
与params
在不断的同步生成,.then()
也在不断的同步注册回调任务。
还记得javascript
中什么是闭包吗?所谓闭包,就是能访问外部函数作用域中自由变量的函数,而此时外部函数很明显就是new Promise(fn)
的fn
,内部函数就是.then()
注册的回调函数,自由变量自然就是上面同步生成的index
了,而闭包的一大特性就是,即便外部上下文已经销毁,它依旧能访问到当时创建它的执行上下文,以及上下文中的那些自由变量(静态作用域的魅力)。
因此即便run()
在不断的执行与销毁,.then()
在注册callback
时这些回调已经自带了它们后续要执行的上下文,这就像人能在地球生活,是因为地球这个上下文提供了空气,水等物质,而宇航员离开了地球依旧能生存,是因为他们自带了氧气等生活物质,即使他们已不在地球这个上下文了。
假设我们断点查看任意一个Promise
执行,你会发现每次执行时都有一个closure
作用域,这就是闭包的英文单词:

若你对闭包以及执行上下文有一定疑惑,可以阅读博主这两篇文章:
一篇文章看懂JS闭包,都要2020年了,你怎么能还不懂闭包?
叁 ❀ 改写实现Promise.all
好了,解释完结果与参数的对应关系后,我们直接改写上述代码,得到我们的PromiseAll
,它满足2个特性:
- 只有所有
Promise
全部resolve
时才会resolve
,且结果顺序与参数保持一致。 - 任意一个失败时直接
reject
。
function PromiseAll(promiseList = []) {
// 用于统计成功的次数
let resLength = 0;
// 浅拷贝一份,原数据的length我们还有用
const pending = [...promiseList];
const resList = [];
// 一定得返回一个promise
return new Promise((resolve, reject) => {
const run = () => {
if (!pending.length) return;
const index = promiseList.length - pending.length;
const promise = pending.shift();
promise.then((res) => {
resLength++;
// 按index来保存结果
resList[index] = res;
// 全部成功了吗?没有就继续请求,否则resolve(resList)跳出递归;
resLength === promiseList.length ? resolve(resList) : run();
})
.catch(reject) // 有一个失败就直接失败
};
// 遍历,模拟前两次依次调用的动作,然后在run内部控制如何执行
promiseList.forEach(() => run());
})
}
执行如下代码,你会发现结果完全符合预期:
const P1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 3000)
});
const P2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(2), 1000)
});
const P3 = new Promise((resolve, reject) => {
setTimeout(() => resolve(3), 2000)
});
PromiseAll([P1, P2, P3]).then((res) => console.log('最终结果为:', res)); // 1 2 3 4 5
假设你将上述三个Promise
中任意一个的状态改为reject
,最终Promise
也只会得到失败的结果,而上述的改写,我们还真的只是去除了限制器的代码,理解起来也非常简单。
肆 ❀ 改写实现Promise.race
race
顾名思义就是赛跑,多个Promise
第一个执行完状态是啥就是啥,所以针对上面的代码,我们又只需要删除掉resLength === promiseList.length
以及递归的相关逻辑即可,直接上代码:
function PromiseRace(promiseList = []) {
// 一定得返回一个promise
return new Promise((resolve, reject) => {
const run = (p) => {
p.then((res) => {
resolve(res);
})
.catch(reject) // 有一个失败就直接失败
};
// 遍历,模拟前两次依次调用的动作,然后在run内部控制如何执行
promiseList.forEach((p) => run(p));
})
}
再运行上面的例子,同样符合预期。
伍 ❀ 总
其实从上篇的文章的题三,到后来的all race
的实现,你会发现难度反而是递减的,所以如果你对于这篇文章存在疑虑,我还是建议阅读下前两篇文章:
因两道Promise执行题让我产生自我怀疑,从零手写Promise加深原理理解
一个思路搞定三道Promise并发编程题,手摸手教你实现一个Promise限制器
建议按顺序阅读这三篇文章,我想你对于Promise
的理解以及手写,一定会上升一个高度,那么到这里本文结束。
【JS】强化Promise理解,从零手写属于自己的Promise.all与Promise.race的更多相关文章
- 史上最完整promise源码手写实现
史上最完整的promise源码实现,哈哈,之所以用这个标题,是因为开始用的标题<手写promise源码>不被收录 promise自我介绍 promise : "君子一诺千金,承诺 ...
- 深入理解 JavaScript 异步系列(3)—— ES6 中的 Promise
第一部分,Promise 加入 ES6 标准 原文地址 http://www.cnblogs.com/wangfupeng1988/p/6515855.html 未经作者允许不得转载! 从 jquer ...
- angular.js的一点理解
对angular.js的一点理解 2015-01-14 13:18 by MrGeorgeZhao, 317 阅读, 4 评论, 收藏, 编辑 最近一直在学习angular.js.不得不说和jquer ...
- 奇舞js笔记——第0课——如何写好原生js代码
摘要 1.好的代码职责要清晰,javscript不要用来操作样式: 2.API要设计的合理:通用性,适度的抽象(数据抽象,过程抽象),可扩展性: 3.效率问题:用好的.合适的算法(前端程序员要把自己当 ...
- 一起学习造轮子(一):从零开始写一个符合Promises/A+规范的promise
本文是一起学习造轮子系列的第一篇,本篇我们将从零开始写一个符合Promises/A+规范的promise,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Pr ...
- promise 理解与总结
对Promise的理解 Promise是异步编程的一种解决方案,可以获取异步操作的消息,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大. 所谓Promise,简单说就是一个容器,里面保 ...
- 前端基本知识(三):JS的闭包理解
JS闭包的理解 一.变量的作用域 二.如何从外部读取局部变量 三.什么是闭包 四.深入理解闭包 五.闭包的用途 六.使用闭包注意情况 七.JavaScript的垃圾回收机制 八.一些思考题 一.变量作 ...
- 前端基本知识(三):JS的闭包理解(第一个思考题有错误,已修改)
JS闭包的理解 一.变量的作用域 二.如何从外部读取局部变量 三.什么是闭包 四.深入理解闭包 五.闭包的用途 六.使用闭包注意情况 七.JavaScript的垃圾回收机制 八.一些思考题 一.变量作 ...
- JS页面跳转代码怎么写?总结了5种方法
我们在建站时有些链接是固定的,比如客服咨询链接,一般是第三方url,如果直接加上去不太专业,那么就想着用站内的页面做跳转,跳转用js比较多,那么JS页面跳转代码怎么写呢?ytkah在网上搜索了一下,大 ...
- 37.js----浅谈js原型的理解
浅谈Js原型的理解 一.js中的原型毫无疑问一个难点,学习如果不深入很容易就晕了! 在参考了多方面的资料后,发现解释都太过专业,对于很多还没有接触过面向对象 语言的小白来说,有理解不了里面 ...
随机推荐
- DEV-C++调试报错
1.报错信息如下: 2.原因 SIGSEGV是是当一个进程执行了一个无效的内存引用,或发生段错误时发送给它的信号. 意思是程序接受一个无效的指针地址,Segmentation fault即是提示我们去 ...
- 【Python】.format用法
格式化打印 print("***{}".format(args)) 格式化转换 module = "skull" print "MODULE_{} ...
- uniapp 子页面 滚动监听 是否到底
主要属性: handleScrollToLower <template> <view class="menu"> <scroll-view id=& ...
- Oracle 不同字符集复合索引长度验证
Oracle 不同字符集复合索引长度验证 背景 前段时间同事找到一个参数, 可以解决Oracle的char和byte 模式存储超长的问题. 很大程度上解决了研发修改SQL的工作量. 但是发现在某些字符 ...
- [转帖]Sqlserver数据库中char、varchar、nchar、nvarchar的区别及查询表结构
https://www.cnblogs.com/liuqifeng/p/10405121.html varchar 和 nvarchar区别: varchar(n)长度为 n 个字节的可变长度且非 U ...
- [转帖]SIMD指令集 SSE/AVX
SIMD指令集 SSE/AVX 概述 参考手册 Intel Intrinsics Guide Tommesani.com Docs Intel 64 and IA-32 Architectures S ...
- Prometheus+alertmanager实现告警的简单验证
Prometheus+alertmanager实现告警的简单验证 背景 学习源自: http://www.mydlq.club/article/126/ 上午没搞定, 中午睡不着,继续学习处理. 发现 ...
- 【转帖】淫技巧 | 如何查看已连接的wifi密码
主题使用方法:https://github.com/xitu/juejin-markdown-themes theme: juejin highlight: github 一.引言 在实际工作中,常常 ...
- [转帖]Linux命令拾遗-%iowait指标代表了什么?
https://www.cnblogs.com/codelogs/p/16060759.html 简介# 一直以来,我都知道top.vmstat.mpstat中有一个叫wa(%iowait)的cpu指 ...
- MYsql备份恢复简单过程
1. 备份数据库 建完数据库更新完补丁之后进行数据库的备份操作. mysqldump -uroot --databases yourdatabase -p > /home/yourdatabas ...