日前拥有将近4k star的 mini-vue 作者大崔哥在朋友圈发了一段这样的话:
那么到底让大崔哥睡不着觉的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.race
,Promise.all
,for of
,Event 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】**中的异步任务
大崔哥的图解
以下是大崔哥朋友圈的图解:
收集任务
达到并行的最大值
如何知道并行任务的完成?
模拟实现运行
接下来我们来模拟实现一下:
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)
控制台的打印输出
执行过程解析:
第一次循环2000
的时候,分别往ret
,executing
里添加了一个异步函数,并向异步任务队列里添加了一个异步任务2000
,到console.log('222')
结束第一次循环,因为下面条件不满足。
第二次循环1000
的时候,又分别往ret
,executing
里添加了一个异步函数,并向异步任务队列里添加了一个异步任务1000
,然后打印输出222
,这个时候executing
里的数量已达到最大并行数,所以继续执行下面的代码,打印输出333
和一个Promise
数据,等待Promise.race(executing)
执行异步任务队列的任务,这个时候异步任务队列里有两个微任务2000
和1000
,因为异步任务队列遵循先进先出,所以先打印target 2000
,再打印target 1000
,然后1000
最先返回所以打印build 1000
,执行then
方法打印executing.splice 1000
,然后删除executing
里的1000
的异步函数,打印444
。
第三次循环3000
的时候,过程跟第二次是一样的。又分别往ret
,executing
里添加了一个异步函数,并向异步任务队列里添加了一个异步任务3000
,然后打印输出222
,这个时候executing
里的数量已达到最大并行数,所以继续执行下面的代码,打印输出333
和一个Promise
数据,等待Promise.race(executing)
执行异步任务队列的任务,这个时候异步任务队列里只有一个异步任务3000
,所以打印target 3000
,然后等待Promise.race(executing)
执行异步任务队列的任务,这个时候有两个任务在执行2000
和3000
,然后2000
最先返回所以打印build 2000
,执行then
方法打印executing.splice 2000
,然后删除executing
里的2000
的异步函数,打印444
第四次循环6500
的时候,过程跟第三次是一样的。
vue3源码的中应用
最后我们来看看这个函数在vue3中是拿来干什么的。
首先这个函数在vue3中的源码的位置是script/build.js
具体内容
然后我搜索整个项目发现只在一处引用了这个函数
async function buildAll(targets) {
await runParallel(require('os').cpus().length, targets, build)
}
那么这个require('os').cpus()
是什么意思呢?这个就要搜查node.js的OS模块了
Nodejs的OS模块
Node.js os 模块提供了一些基本的系统操作函数。我们可以通过以下方式引入该模块:
序号 | 方法 | 描述 |
---|---|---|
1 | os.tmpdir() | 返回操作系统的默认临时文件夹。 |
2 | os.endianness() | 返回 CPU 的字节序,可能的是 “BE” 或 “LE”。 |
3 | os.hostname() | 返回操作系统的主机名。 |
4 | os.type() | 返回操作系统名 |
5 | os.platform() | 返回编译时的操作系统名 |
6 | os.arch() | 返回操作系统 CPU 架构,可能的值有 “x64”、“arm” 和 “ia32”。 |
7 | os.release() | 返回操作系统的发行版本。 |
8 | os.uptime() 返回操作系统运行的时间,以秒为单位。 | |
9 | os.loadavg() | 返回一个包含 1、5、15 分钟平均负载的数组。 |
10 | os.totalmem() | 返回系统内存总量,单位为字节。 |
11 | os.freemem() | 返回操作系统空闲内存量,单位是字节。 |
12 | os.cpus() | 返回一个对象数组,包含所安装的每个 CPU/内核的信息:型号、速度(单位 MHz)、时间(一个包含 user、nice、sys、idle 和 irq 所使用 CPU/内核毫秒数的对象)。 |
13 | os.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文件并且不能是私有的和不能没有打包选项
最后的build函数,就是具体的打包执行函数。
总结:
通过Promise池允许通过等待Promise来限制并行运行的最大数量。