阅读p-limit源码

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来使用队列,这里我们为了方便理解直接使用数组的pushshift方法来模拟。

我们首先看下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()();
			}
		})();
	};

这个函数做了这几件事

  1. 将任务追加到队列中
  2. (await Promise.resolve)确保当前的微任务队列被清空,以便next函数可以在队列中添加下一个任务之前执行
  3. 如果当前执行函数的数量小于我们设置的并发数,则立即执行一次queue.shift()(),其实也就是执行run方法

接下来看看run方法的源码

const run = async (fn, resolve, args) => {
		activeCount++;

		const result = (async () => fn(...args))();

		resolve(result);

		try {
			await result;
		} catch {}

		next();
	};

这三件事情为顺序执行:

  1. 让 activeCount +1
  2. 执行异步函数 fn,并将结果传递给 resolve a. 为保证 next 的顺序,采用了 await result
  3. 调用 next 函数

函数 next 做两件事情

  1. 让 activeCount -1
  2. . 当队列中还有元素时,弹出一个元素并执行,按照上面的逻辑,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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值