介紹
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/']
預設的可迭代物件有String
、Array
、TypedArray
、Map
、Set
、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
原始碼中提供了 pMapSkip
,pMap5kip
是一個 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
,改為用新陣列接收;push
比 splice
效能好;
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
到裡面;signal
是 AbortController
的例項屬性;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
核心功能實現完了;感興趣的同學可以點點贊;