介绍

p-map 是一个迭代处理 promise 并且能控制 promise 执行并发数的库。作者是 sindresorhus,他还创建了许多关于 promise 的库 promise-fun,感兴趣的同学可以去看看。

之前 提到的 p-limit 也是一个控制请求并发数的库,控制并发数方面,两者作用相同,不过 p-map 增加了对请求(promise)的迭代处理。

之前 p-limit 的用法如下,limit 接受一个函数;

var limit = pLimit(8); // 设置最大并发数量为 2
var input = [ // Limit函数包装各个请求
limit(() => fetchSomething('1')),
limit(() => fetchSomething('2')),
limit(() => fetchSomething('3')),
limit(() => fetchSomething('4'))
];
// 执行请求
Promise.all(input).then(res =>{
console.log(res)
})

p-map 则通过用户传进来的 mapper 处理函数处理的一个集合(准确的说是一个可迭代对象);

import pMap from 'p-map';
import got from 'got'; const sites = [
getWebsiteFromUsername('sindresorhus'), //=> Promise
'https://avajs.dev',
'https://github.com'
]; const mapper = async site => {
const {requestUrl} = await got.head(site);
return requestUrl;
}; // 接收三个参数,一个是可迭代对象,一个是对可迭代对象进行处理的函数,一个是配置选项
const result = await pMap(sites, mapper, {concurrency: 2}); console.log(result);
//=> ['https://sindresorhus.com/', 'https://avajs.dev/', 'https://github.com/']

默认的可迭代对象有StringArrayTypedArrayMapSet 、Intl.Segments,而要成为可迭代对象,该对象必须实现 [Symbol.iterator]() 方法;

遍历可迭代对象时,实际上是根据迭代器协议进行遍历;

比如,迭代一个数组是这样的

var iterable = [1,2,3,4]
var iterator = iterable[Symbol.iterator]() // iterator 是迭代器
iterator.next() // {value: 1, done: false}
iterator.next() // {value: 2, done: false}
iterator.next() // {value: 3, done: false}
iterator.next() // {value: 4, done: false}
iterator.next() // {value: undefined, done: true}

当数组迭代完成后,会返回 {value: undefined, done: true}

p-pap 控制并发请求的原理是,对传进来的集合进行迭代,当集合第一个元素(元素可能是异步函数)执行完后,会交给 mapper 函数处理,mapper 处理完后,才开始迭代下一个元素,这样就保持了按照顺序一个个迭代,此时并发数是1

要做到并发是n,并且还能执行上面的迭代,作者很巧妙的用了 for 循环

for(let i=0;i<n;i++)
next()
}

手写 p-map

下面按照作者思路实现一个 p-map,现在有一个需要处理的可迭代对象 arr

var fetchSomething = (str,ms) =>{
return new Promise((resolve,reject) =>{
setTimeout(() =>{
resolve(parseFloat(str))
},ms)
})
} var arr= [
fetchSomething('1a' ,1000), // promise
2,3,
fetchSomething( '4a' , 5000), // promise
5,6,7,8,9,10
]

集合第一个元素和第四个元素是一个 promise 函数

p-map 接收三个参数,分别是要迭代的对象,mapper 处理函数,自定义配置;返回值是 promise, 如下面所示

var pMap = (iterable, mapper, options) => new Promise((resolve,reject) => {});

拿到可迭代对象后,对它进行递归迭代,直至迭代完毕;这里定义一个内部递归迭代函数 next

var pMap = (iterable, mapper, options) => new Promise((resolve,reject) => {
var iterator = iterable[Symbol.iterator]() var next= ()=>{
var item=iterator.next()
if(item.done){
return
}
next()
} });

迭代对象中每个元素都是按顺序迭代的;如果元素是异步函数时,需要先等异步函数兑现,并且兑现后的值传给 mapper 函数,等到 mapper 函数兑现或者拒绝后才继续迭代下一个元素

var iterator = iterable[Symbol.iterator]()
var next = () => {
var item = iterator.next() if (item.done) {
return
} Promise.resolve(item.value)
.then(res => mapper(res))
.then(res2 => {
next()
})
}

并且每次迭代彻底完成后保存兑现的结果

var iterator = iterable[Symbol.iterator]()
var index = 0 // 序号,根据可迭代对象的顺序保存结果
var ret = []// 保存结果
var next = () => {
var item = iterator.next() if (item.done) {
return
} var currentIndex = index //保存当前元素序号,用于存入结果
index++ //下一个元素的序号
Promise.resolve(item.value)
.then(res => mapper(res))
.then(res2 => {
ret[currentIndex] = res2
next()
})
}

当整个迭代完后,并且元素全部执行(兑现)完,输出结果集

var pMap = (iterable, mapper, options) => new Promise((resolve,reject) => {
var activeCount = 0 //正在执行的元素个数
var next = () => {
var item = iterator.next()
if (item.done) { //元素全部迭代完
if (activeCount == 0) {
resolve(ret) //元素全部执行(兑现)完,输出结果集
}
return
}
var currentIndex = index // 保存当前元素序号,用干存入结果
index++ //下一个元素的序号
activeCount++
Promise.resolve(item.value)
.then(res => mapper(res))
.then(res2 => {
ret[currentIndex] = res2
activeCount--
next()
})
.catch(err => {
activeCount--
})
}
})
配置项 stopOnError

传入 p-map 配置项中有一个参数是 stopOnError,表示当执行遇到错误,是否终止迭代循环,所以这里在 .catch() 里面做判断;

Promise.resolve(item.value)
.then(res = mapper(res))
.then(res2 => {
// ...
})
.catch(err => {
ret[currentIndex] == err // 将错误的结果也保存起来 if (stopOnError) {
hasError = true
reject(err) // 发生错误,终止循环
}else {
hasError = false
activeCount--
next()
}
}
忽略错误执行结果 pMapSkip

mapper 函数是用户自定义的, 如果 mapper 执行错误,用户期望忽略错误执行结果,只保留正确结果,这该怎么做呢?,此时 pMapSkip 就登场了;

p-map 源码中提供了 pMapSkippMap5kip 是一个 Symbol 值,p-map 内部处理则是:当结果集收到的结果是 pMapSkip,则会在迭代完成后清除返回值是 pMapSkip 的元素,也就是说 mapper 处理时发生错误, 用户不想要这个值,可以 reject(pMapSkip) 比如:

import pMap, { pMapSkip } from 'p-map'

var arr = [
fetchSomething('1a', 1000, true),
2, 3
] var mapper = (item, index) => {
return new Promise((resolve, reject) => {
return item == 2 ? reject(pMapSkip): resolve(parseFloat(item)) // 元素是 2 ,抛出错误
})
} (async () => {
const result = await pMap(arr, mapper, { concurrency: 2 });
console.log(result); //=>[1,3]
})();

所以当 mapper 返回 pMapSkip 时,需要标记对应的元素

var skipIndexArr= []

记录需要剔除的元素的位置

var skipIndexArr = [];

Promise.resolve(item.value)
.then((res = mapper(res)))
.then((res2) => {
// ...
})
.catch((err) => {
if (err === pMapSkip) {
skipIndexArr.push(currentIndex); //记录需要剔除的元素的位置
} else {
ret[currentIndex] == err; if (stopOnError) {
hasError = true;
reject(err);
} else {
hasError = false;
activeCount--;
next();
}
}
});

并且在迭代结束时剔除结果集中的有 pMapSkip 的元素

if (item.done) {
if (activeCount == 0) {
for (var k of skipIndexArr) {
ret.splice(k, 1);
}
resolve(ret);
return;
}
}

在数据里大的情况下,频繁使用 splice 性能可能没那么好,因为执行 splice 后,其后的元素的索引都会改变;那么就要改造下,将 skipIndexArr 改为 Map 形式。

// var skipIndexArr= []
var skipIndexArr= new Map()

记录需要删除的元素的位置

if (err === pMapSkip) {
skipIndexArr.set(currentIndex, err);
}

然后迭代结束时,不再在原数组里面 splice ,改为用新数组接收;pushsplice 性能好;

if (item.done) {
if (activeCount == 0) {
if (skipIndexArr.size === 0) {
resolve(ret);
return;
} const pureRet = []; for (const [index, value] of ret.entries()) {
if (skipIndexArr.get(index) === pMapSkip) {
continue;
} pureRet.push(value);
} resolve(pureRet);
}
return;
}
在外部取消 p-map 的请求或者取消迭代: AbortController

存在某些情况,当我们不再需要 p-map 返回的结果,或者不再想要使用 p-map 时,我们就需要在外部取消 p-map 的请求或者取消迭代,这时就可以使用 AbortController

简单介绍下 AbortController 的用法,有一个请求 fetchSomething

var fetchSomething = (str) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(str)
}, 1000)
})
}

想要取消 fetchSomething 请求,就需要传一个 signal 到里面;signalAbortController 的实例属性;AbortController 和请求之间就是由 signal 建立关联;

var controller = new AbortController()
var signal = controller.signal var fetchSomething = (str,signal) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(str)
}, 1000)
})
} fetchSomething('fetch',signal).then(res => {
console.log('res:', res)
}).catch(err => {
console.log('err:', err)
})

建立关联后,外部取消请求使用的是 AbortController 实例方法 controller.abort()

controller.abort()

然后在请求里面监听外部是否调用了 controller.abort();有两种方式

signal.addEventListener('abort', () => {

}, false)

或者

if (signal.aborted) {

}

完整示例:

var controller = new AbortController()
var signal = controller.signal var fetchSomething = (str,signal) => {
return new Promise((resolve, reject) => {
signal.addEventListener('abort', () => {
console.log(' addEventListener')
reject('addEventListener取消')
}, false)
setTimeout(() => {
if (signal.aborted) {
console.log('aborted')
reject('aborted取消')
return
}
console.log('进入setTimeout')
resolve(str)
}, 1000)
})
} setTimeout(() => {
controller.abort()
}, 500) fetchSomething('fetch',signal).then(res => {
console.log('res:', res)
}).catch(err => {
console.log('err:', err)
})

500ms 后输出:

addEventListener
err: addEventListener取消
aborted

结合 p-map 使用如下:

import pMap from 'p-map';

const abortController = new AbortController();

setTimeout(() => {
abortController.abort();
}, 500); const mapper = async value => value; await pMap([fetchSomething(1000), fetchSomething(1000)], mapper, {signal: abortController.signal});
// 500 ms 结束 pMap 方法,抛出错误信息.

那么 p-map 内部实现就好写了:

var pMap = (iterable, mapper, options) => new Promise((resolve, reject) => {
var { signal } = options if (signal) {
if (signal.aborted) {
reject(signal.reason);
} signal.addEventListener('abort', () => {
reject(signal.reason);
});
} var next = () =>{
//...
}
})
手写的完整源码:

下面的源码也可以把 promise.then() 和 .catch() 写法改进为 async await + try catch 写法;

let pMapSkip = Symbol('skip')

var pMap = (iterable, mapper, options) => new Promise((resolve, reject) => {
var iterator = iterable[Symbol.iterator]()
var index = 0 // 序号,根据可迭代对象的顺序保存结果
var ret = []// 保存结果
var activeCount = 0 //正在执行的元素个数
var isIterableDone = false
var hasError = false
var skipIndexArr = new Map()
var { signal, stopOnError, concurrency } = options if (signal) {
if (signal.aborted) {
reject(signal.reason);
} signal.addEventListener('abort', () => {
reject(signal.reason);
});
} var next = () => {
var item = iterator.next()
if (item.done) {
isIterableDone = true
if (activeCount == 0) {
if (skipIndexArr.size === 0) {
resolve(ret);
return;
} const pureRet = []; for (const [index, value] of ret.entries()) {
if (skipIndexArr.get(index) === pMapSkip) {
continue;
} pureRet.push(value);
} resolve(pureRet);
}
return;
}
var currentIndex = index // 保存当前元素序号,用干存入结果
index++ //下一个元素的序号
activeCount++
Promise.resolve(item.value)
.then(res => mapper(res))
.then(res2 => {
ret[currentIndex] = res2
activeCount--
next()
})
.catch(err => {
ret[currentIndex] == err;
if (stopOnError) {
hasError = true;
reject(err);
} else {
ret[currentIndex] == err;
if (err === pMapSkip) {
skipIndexArr.set(currentIndex, err);
}
hasError = false;
activeCount--;
next();
} })
} for (let k = 0; k < concurrency; k++) {
if (isIterableDone) {
break
}
next()
}
})
测试一下

1、测试 pMapSkip

var arr= [
fetchSomething('1a' ,1000), // promise
2,3,
fetchSomething( '4a' , 5000), // promise
5,6,7,8,9,10
]
var mapper = (item, index) => {
return new Promise((resolve, reject) => {
return item == 3 ? reject(pMapSkip): resolve(parseFloat(item))
})
} pMap(arr, mapper, { concurrency: 2 }).then(res => {
console.log(res) // [1, 2, 4, 5, 6, 7, 8, 9, 10] ,剔除了 3
})

2、测试中止请求

var controller = new AbortController()
var signal = controller.signal pMap(arr, mapper, { concurrency: 2,signal:signal }).then(res => {
console.log(res)
}).catch(err =>{
console.log(err) // 500ms 后打印 AbortError: signal is aborted without reason
}) setTimeout(() =>{
controller.abort()
},500)

3、测试 stopOnError

pMap(arr, mapper, { concurrency: 2,signal:signal,stopOnError:true }).then(res => {
console.log(res)
}).catch(err =>{
console.log(err) // Symbol(skip)
})

至此,p-map 核心功能实现完了;感兴趣的同学可以点点赞;

手写 p-map(控制并发数以及迭代处理 promise 的库)的更多相关文章

  1. 用python实现的的手写数字识别器

    概述 带GUI界面的,基于python sklearn knn算法的手写数字识别器,可用于识别手写数字,训练数据集为mnist. 详细 代码下载:http://www.demodashi.com/de ...

  2. JUC 并发编程--05, Volatile关键字特性: 可见性, 不保证原子性,禁止指令重排, 代码证明过程. CAS了解么 , ABA怎么解决, 手写自旋锁和死锁

    问: 了解volatile关键字么? 答: 他是java 的关键字, 保证可见性, 不保证原子性, 禁止指令重排 问: 你说的这三个特性, 能写代码证明么? 答: .... 问: 听说过 CAS么 他 ...

  3. 【Spring】手写Spring MVC

    Spring MVC原理 Spring的MVC框架主要由DispatcherServlet.处理器映射.处理器(控制器).视图解析器.视图组成. 完整的Spring MVC处理 流程如下: Sprin ...

  4. Atitit s2018.2 s2 doc list on home ntpc.docx  \Atiitt uke制度体系 法律 法规 规章 条例 国王诏书.docx \Atiitt 手写文字识别 讯飞科大 语音云.docx \Atitit 代码托管与虚拟主机.docx \Atitit 企业文化 每日心灵 鸡汤 值班 发布.docx \Atitit 几大研发体系对比 Stage-Gat

    Atitit s2018.2 s2 doc list on home ntpc.docx \Atiitt uke制度体系  法律 法规 规章 条例 国王诏书.docx \Atiitt 手写文字识别   ...

  5. 透彻理解Spring事务设计思想之手写实现

    前言 事务,是描述一组操作的抽象,比如对数据库的一组操作,要么全部成功,要么全部失败.事务具有4个特性:Atomicity(原子性),Consistency(一致性),Isolation(隔离性),D ...

  6. 透彻理解Spring事务设计思想之手写实现(山东数漫江湖)

    前言 事务,是描述一组操作的抽象,比如对数据库的一组操作,要么全部成功,要么全部失败.事务具有4个特性:Atomicity(原子性),Consistency(一致性),Isolation(隔离性),D ...

  7. 手写一个类SpringBoot的HTTP框架:几十行代码基于Netty搭建一个 HTTP Server

    本文已经收录进 : https://github.com/Snailclimb/netty-practical-tutorial (Netty 从入门到实战:手写 HTTP Server+RPC 框架 ...

  8. 手写一个RPC框架

    一.前言 前段时间看到一篇不错的文章<看了这篇你就会手写RPC框架了>,于是便来了兴趣对着实现了一遍,后面觉得还有很多优化的地方便对其进行了改进. 主要改动点如下: 除了Java序列化协议 ...

  9. Nodejs - 如何用 eventproxy 模块控制并发

    本文目标 本文的目标是获取 ZOJ 1001-1010 每道题 best solution 的作者 id,取得数据后一次性输出在控制台. 前文 如何用 Nodejs 分析一个简单页面 我们讲了如何用 ...

  10. Spring系列之手写一个SpringMVC

    目录 Spring系列之IOC的原理及手动实现 Spring系列之DI的原理及手动实现 Spring系列之AOP的原理及手动实现 Spring系列之手写注解与配置文件的解析 引言 在前面的几个章节中我 ...

随机推荐

  1. 《Programming from the Ground Up》阅读笔记:p88-p94

    <Programming from the Ground Up>学习第5天,p88-p94总结,总计7页. 一.技术总结 1.touppercase.s #PURPOSE: This pr ...

  2. Bond4配置

    Bongding聚合链路工作模式 > bond聚合链路模式共7种:0-6Mode > bond 0 负载均衡 轮询方式往每条链路发送报文,增加带宽和容错能力.容易出现数据包无序到达的问题, ...

  3. Vue仿微信PC客户端

    一个模仿PC微信客户端的纯前端vue项目 项目地址 github: https://github.com/TomHusky/vue-wechat-demo gitee: https://gitee.c ...

  4. EdgeOne安全专项实践:上传文件漏洞攻击详解与防范措施

    前言 今天,我们将深入探讨上传文件漏洞攻击,这部分内容是EdgeOne专项实践篇的一部分.在本章中,我们不会涉及文件漏洞的含义.原理或站点配置等基础教程,如果你对这些内容感兴趣,可以参考这篇文章:探索 ...

  5. DNS在架构中的使用

    1 介绍 DNS(Domain Name System,域名系统)是一种服务,它是域名和IP地址相互映射的一个分布式数据库,能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的IP地址数串. ...

  6. 使用Git bash切换Gitee、GitHub多个Git账号

    使用Git bash切换Gitee.GitHub多个Git账号 ​ Git是分布式代码管理工具,使用命令行的方式提交commit.revert回滚代码.这里介绍使用Git bash软件来切换Gitee ...

  7. Vue打包部署到CentOS 7

    项目打包 在项目目录下执行打包目录进行打包 yarn build // 或者 npm run build 打包完成后会生成一个dist文件夹,这样就打包完成了(我这样做了SEO的,所有目录结构有点不一 ...

  8. 7月22号python 每日一题

    7月22号python 每日一题 LCR 121. 寻找目标值 - 二维数组 难度:中等 m*n 的二维数组 plants 记录了园林景观的植物排布情况,具有以下特性: 每行中,每棵植物的右侧相邻植物 ...

  9. baselines算法库common/atari_wrappers.py模块分析

    common/atari_wrappers.py模块代码如下: import numpy as np import os os.environ.setdefault('PATH', '') from ...

  10. [CEOI2007] 树的匹配 Treasury 题解

    前言 题目链接:洛谷. 题目简述 给一棵树,问你这棵树的最大匹配是多少,并且计算出有多少种最大匹配. 题目分析 先来考虑较简单的最大匹配数.对于某一个结点,它有以下三种状态: 不参与匹配: 和某一个儿 ...