vue3源码打包中的并行可设置最大值的实现详解

2,250 阅读7分钟

日前拥有将近4k star的 mini-vue 作者大崔哥在朋友圈发了一段这样的话:

00.jpg

那么到底让大崔哥睡不着觉的vue3源码长啥样的呢?

vue3源码中的巧妙代码

async function runParallel(maxConcurrency, source, iteratorFn) {
    const ret = []
    const executing = []
    for(const item of source){
        const p = Promise.resolve().then(() => iteratorFn(item, source))
        ret.push(p)
        if(maxConcurrency <= source.length) {
           const e = p.then(() => executing.splice(executing.indexOf(e), 1))
           executing.push(e)
            if(executing.length >= maxConcurrency) {
               await Promise.race(executing)
            }
        }
    }
    return Promise.all(ret)
}

使用到的基础知识

在上面这段vue3的打包代码中有几个非常重要的知识点需要掌握 :Promise.racePromise.allfor ofEvent loop

Pomise.all的使用

Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

Promise.race的使用

顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。

for in、for of 区别

for in遍历的是数组的索引(即键名),而for of遍历的是数组元素值

Event loop复习

Event loop网上已经有很多教程了,我这里只做简单的复习,也当作我本人的复习。因为JavaScript是一门单线程的语言,所以同一时间内只能做一件事情,要做其他事情就要放到异步任务队列中,等当前执行栈的代码执行完毕之后,就会去异步任务队列里把异步任务提取到执行栈中执行,如果当前执行栈又产生异步任务,又把异步任务放到任务队列中去,等执行栈的代码执行完后再来调取,如此循环,就是Event loop。需要注意的是:异步任务分为,宏任务和微任务,微任务先执行,而且会把异步任务队列里的所有微任务都会执行完清空,微任务执行完之后,浏览器会进行重绘,然后再执行异步任务队列里的宏任务,异步任务遵循先进先出。

runParallel函数详细解读

接下来我们详细解读一下这段代码

maxConcurrency 最大并行执行数

source 异步任务数组

iteratorFn 异步任务执行函数

const ret = [] 构造一个异步执行函数的数组供给Promise.all调用。

const executing = [] 并行执行的异步任务队列数组,大崔哥管它叫:并行池

for(const item of source) 循环异步任务

const p = Promise.resolve().then(() => iteratorFn(item, source)) 往异步任务队列里添加任务

ret.push(p) 构造 Promise.all 调用执行的异步函数数组

if(maxConcurrency <= source.length) 如果最大并行执行数小于或等于异步任务数,则不需要进行限制

const e = p.then(() => executing.splice(executing.indexOf(e), 1)) 构建异步任务执行回调后的then方法,在then方法里删除**并行执行的异步任务队列数组 【executing】 **中已经执行并返回结果的异步任务

executing.push(e)并行池里添加异步任务

if(executing.length >= maxConcurrency) 当并行执行的异步任务数已经大于或等于最大并行执行数时

await Promise.race(executing) 执行**并行执行的异步任务队列数组 【executing】**中的异步任务

大崔哥的图解

以下是大崔哥朋友圈的图解:

收集任务

01.jpg

达到并行的最大值

02.jpg

如何知道并行任务的完成?

03.jpg

模拟实现运行

接下来我们来模拟实现一下:

async function runParallel(maxConcurrency, source, iteratorFn) {
    const ret = []
    const executing = []
    for (const item of source) {
        const p = Promise.resolve().then(() => iteratorFn(item, source))
        ret.push(p)
        if (maxConcurrency <= source.length) {
            const e = p.then(() =>{
                console.log('executing.splice', item)
                executing.splice(executing.indexOf(e), 1)
            })
            executing.push(e)
            console.log('222')
            if (executing.length >= maxConcurrency) {console.log('333', e)
                 await Promise.race(executing)
                 console.log('444')
            }
        }
    }
    return Promise.all(ret)
}
// 模拟实现代码
const source = [2000,1000,3000,6500]
async function build(target) {console.log('target', target)
    await new Promise((resolve, reject) => {
         setTimeout(() => {
            console.log('build', target)
            resolve('success')    
         }, target);
    })
}
runParallel(2, source, build)

控制台的打印输出

01..png

执行过程解析:

第一次循环2000的时候,分别往retexecuting里添加了一个异步函数,并向异步任务队列里添加了一个异步任务2000,到console.log('222')结束第一次循环,因为下面条件不满足。

第二次循环1000的时候,又分别往retexecuting里添加了一个异步函数,并向异步任务队列里添加了一个异步任务1000,然后打印输出222,这个时候executing里的数量已达到最大并行数,所以继续执行下面的代码,打印输出333和一个Promise数据,等待Promise.race(executing)执行异步任务队列的任务,这个时候异步任务队列里有两个微任务20001000,因为异步任务队列遵循先进先出,所以先打印target 2000,再打印target 1000,然后1000最先返回所以打印build 1000,执行then方法打印executing.splice 1000,然后删除executing里的1000的异步函数,打印444

第三次循环3000的时候,过程跟第二次是一样的。又分别往retexecuting里添加了一个异步函数,并向异步任务队列里添加了一个异步任务3000,然后打印输出222,这个时候executing里的数量已达到最大并行数,所以继续执行下面的代码,打印输出333和一个Promise数据,等待Promise.race(executing)执行异步任务队列的任务,这个时候异步任务队列里只有一个异步任务3000,所以打印target 3000,然后等待Promise.race(executing)执行异步任务队列的任务,这个时候有两个任务在执行20003000,然后2000最先返回所以打印build 2000,执行then方法打印executing.splice 2000,然后删除executing里的2000的异步函数,打印444

第四次循环6500的时候,过程跟第三次是一样的。

vue3源码的中应用

最后我们来看看这个函数在vue3中是拿来干什么的。

首先这个函数在vue3中的源码的位置是script/build.js

02.png

具体内容

03.png

然后我搜索整个项目发现只在一处引用了这个函数

async function buildAll(targets) {
  await runParallel(require('os').cpus().length, targets, build)
}

那么这个require('os').cpus()是什么意思呢?这个就要搜查node.js的OS模块了

Nodejs的OS模块

Node.js os 模块提供了一些基本的系统操作函数。我们可以通过以下方式引入该模块:

序号方法描述
1os.tmpdir()返回操作系统的默认临时文件夹。
2os.endianness()返回 CPU 的字节序,可能的是 “BE” 或 “LE”。
3os.hostname()返回操作系统的主机名。
4os.type()返回操作系统名
5os.platform()返回编译时的操作系统名
6os.arch()返回操作系统 CPU 架构,可能的值有 “x64”、“arm” 和 “ia32”。
7os.release()返回操作系统的发行版本。
8os.uptime() 返回操作系统运行的时间,以秒为单位。
9os.loadavg()返回一个包含 1、5、15 分钟平均负载的数组。
10os.totalmem()返回系统内存总量,单位为字节。
11os.freemem()返回操作系统空闲内存量,单位是字节。
12os.cpus()返回一个对象数组,包含所安装的每个 CPU/内核的信息:型号、速度(单位 MHz)、时间(一个包含 user、nice、sys、idle 和 irq 所使用 CPU/内核毫秒数的对象)。
13os.networkInterfaces()获得网络接口列表。

故我们知道require('os').cpus().length 代表的是当前系统的CPU数量,所以vue3源码的打包并行数量是当前系统的CPU数量。

参数targets代表什么意思

那么第二个参数targets又代表什么意思呢?查看源码targets是由一下代码产生的

const targets = (exports.targets = fs.readdirSync('packages').filter(f => {
  if (!fs.statSync(`packages/${f}`).isDirectory()) {
    return false
  }
  const pkg = require(`../packages/${f}/package.json`)
  if (pkg.private && !pkg.buildOptions) {
    return false
  }
  return true
}))

这段代码的意思是读取packages包里目录,并且目录中有package.json文件并且不能是私有的和不能没有打包选项

04.png

最后的build函数,就是具体的打包执行函数。

总结:

通过Promise池允许通过等待Promise来限制并行运行的最大数量。