p-limit介绍
p-limit
是一个控制并发量的库,比如我们在请求接口时同时请求了10个接口,这时候我们希望把十个请求分成两份,每次请求5个,避免服务器太大压力,那我们就可以用到p-limit这个库了。
import pLimit from 'p-limit'
const limit = pLimit(2)
const fetchSomething = (val) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(val, Date.now())
resolve(val)
}, 1000)
})
}
const input = [
limit(() => fetchSomething('foo')),
limit(() => fetchSomething('bar')),
limit(() => fetchSomething('end'))
]
// Only one promise is run at once
const result = await Promise.all(input)
在上面这段用例中,会先弹出foo,bar,然后又隔了一秒(settimeout里面设置的间隔)才弹出end,说明我们的并发控制完成了。
在项目中配合axios使用:
import axios from 'axios';
import pLimit from 'p-limit';
const service = axios.create({
headers: {
'access-control-allow-origin': '*',
'content-type': 'application/json',
},
baseUrl: 'xxx',
withCredentials: true
})
// 请求拦截
service.interceptors.request.use(
)
// 响应拦截
service.interceptors.response.use(
)
const limit = pLimit(5);
const myAxios = (...data) => limit(() => service(...data));
export default myAxios;
源码分析
p-limit
的源码很短只有几十行,其中使用了queue队列结构来维护异步队列,队列是一种先进先出的结构,在源码中借助了yocto-queue
来使用队列,这里我们为了方便理解直接使用数组的push
和shift
方法来模拟。
我们首先看下p-limit的用法:
const limit = pLimit(2)
const input = [
limit(() => fetchSomething('foo')),
limit(() => fetchSomething('bar')),
limit(() => fetchSomething('end'))
]
const result = await Promise.all(input)
由此可见当我们调用pLimit时,应该先传入一个参数(concurrency),这个参数表示一次性执行异步函数的最大并发数,在上面这个例子中是2
并且根据上面的例子我们知道,调用pLimit
后,我们会得到一个函数,因为在下面的input
数组中有三个limit(...)
我们还知道每个 limit(() => fetch())
返回的会是一个promise
综上所述,我们可以知道pLimit
的入口会是一个函数,并且这个函数会返回promise对象。
那么在源码中这个函数就是generator
function pLimit(concurrency) {
function generator(fn, ...args) {
return new Promise(...)
}
return generator
}
generator, enqueue, run, next 四个函数
刚刚我们知道了入口是generator
,并且知道了这个函数返回了promise
对象,现在我们看下它往下走会走到哪一个函数。
function pLimit(concurrency) {
function generator(fn, ...args) {
return new Promise(resolve => {
enqueue(fn, resolve, args)
})
}
return generator
}
这里可以看到,源码中在返回promise的函数里,执行了enqueue
这个方法,这个方法是把刚才const inpu = [...]
中这些异步动作,追加到queue
这个队列中,enqueue函数的源码如下
const enqueue = (fn, resolve, args) => {
queue.push(run.bind(undefined, fn, resolve, args)); // 追加到队列中
(async () => {
await Promise.resolve();
if (activeCount < concurrency && queue.size > 0) {
queue.shift()();
}
})();
};
这个函数做了这几件事
- 将任务追加到队列中
- (await Promise.resolve)确保当前的微任务队列被清空,以便next函数可以在队列中添加下一个任务之前执行
- 如果当前执行函数的数量小于我们设置的并发数,则立即执行一次
queue.shift()()
,其实也就是执行run
方法
接下来看看run方法的源码
const run = async (fn, resolve, args) => {
activeCount++;
const result = (async () => fn(...args))();
resolve(result);
try {
await result;
} catch {}
next();
};
这三件事情为顺序执行:
- 让 activeCount +1
- 执行异步函数 fn,并将结果传递给 resolve a. 为保证 next 的顺序,采用了 await result
- 调用 next 函数
函数 next 做两件事情
- 让 activeCount -1
- . 当队列中还有元素时,弹出一个元素并执行,按照上面的逻辑,run 就会被调用
const next = () => {
activeCount--
if (queue.length) {
queue.shift()()
}
}
就是说,在generator -> enqueue之后,run()和next会交替执行,直到队列中所有异步任务都被清空。
完整代码
// pLimit函数,首先有一个queue的队列,还有一个activeCount的数,来记录当前队列要执行的count
// 一共有四个方法,分别是next,run,enqueue和generator
// 最开始是走generator,将传入的fn放到queue中,并尝试执行一次
// 然后就是run和next的交替进行,next是当期queue为空时,往里面追加
function pLimit(concurrency) {
let queue = []
let activeCount = 0
const next = () => {
activeCount--
if (queue.length) {
queue.shift()()
}
}
// 函数 run 做 3 件事情,这三件事情为顺序执行: i . 让 activeCount +1
// ii . 执行异步函数 fn,并将结果传递给 resolve a. 为保证 next 的顺序,采用了 await result
// iii. 调用 next 函数
const run = async (fn, resolve, args) => {
activeCount++
const result = (async () => fn(...args))() // 将Promise的状态从pending改为resolved,并将结果设置为result。
resolve(result) // 将Promise的状态从pending改为resolved,并将结果设置为result。
try {
await result // 为保证 next 的顺序,采用了 await result。
} catch {}
next()
}
const enqueue = async (fn, resolve, args) => {
queue.push(run.bind(null, fn, resolve, args))
// 1. 它确保当前的微任务队列被清空,以便next函数可以在队列中添加下一个任务之前执行。
// 2. 其次,它允许JavaScript引擎在添加下一个任务之前立即返回。这样,如果活动任务数量仍小于并发限制,则可以尽快开始下一个任务。
await Promise.resolve()
if (activeCount < concurrency && queue.length) {
queue.shift()()
}
}
const generator = (fn, ...args) =>
new Promise((resolve) => {
enqueue(fn, resolve, args)
})
return generator
}
const limit = pLimit(2)
const fetchSomething = (val) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(val, Date.now())
resolve(val)
}, 1000)
})
}
const input = [
limit(() => fetchSomething('foo')),
limit(() => fetchSomething('bar')),
limit(() => fetchSomething('end'))
]
// Only one promise is run at once
const result = await Promise.all(input)
参考http://www.manongjc.com/detail/59-hksdtrnnklcuaec.html