前言
在前面三篇文章中,我们已经大致聊清楚了Rollup的构建流程,构建完成之后,其实这时候内存里面已经充斥了各个绑定着文件内容的Module类实例,接着要做的输出流程把这些内存中的数据写入到磁盘即可完成整个的前端工程构建工作。
之前我们已经聊过了,Rollup的Module类是管辖文件内容的类,但是最终内容会被合并起来,按Chunk输出,之前我们已经看到过Chunk这个类,接下来的篇幅里面,我们将着重和Chunk和Bundle类打交道。
好的,我废话就不多说了,接下来本文将开始向大家阐述Rollup的输出流程。
排序Module执行流程
首先我们先接上文,在这个位置已经拿到了所有的文件内容。
然后就要开始进行Module的排序。
为什么Module需要排序呢?很简单一个道理,JS的引用有先后顺序,如果我们事先访问一个不存在的变量,后面再去申明的话,是有可能报错的。
那么,又该怎么进行排序呢?假设我们有3个JS,分别是a.js->b.js->c.js这样的依赖关键,那么,因为c.js不依赖任何内容,所以c.js应该是可以最先被输出的,然后b.js依赖c.js,所以接下来就可以把b.js输出,因为a.js依赖b.js和c.js,所以a.js应该是最后被输出的。
这个过程,在数据结构里面有一个专题叫做拓扑排序,我曾经写过一篇关于拓扑排序的文章大家可以作为参考学习:拓扑排序在前端开发中的应用场景。
拓扑排序的使用条件是有向无环图(DAG),但是实际开发中是有可能写出循环依赖的,因此,Rollup内部肯定需要做一些错误case的判定。
接下来就看一下Rollup的拓扑排序的过程。
我们得认真仔细看一下analyseModuleExecution这个函数,这个函数内部定义了一个analyseModule函数,这个函数是用来做递归调用的,因为牵涉到递归调用,理解起来相对不是那么直观。
第一个for语句,依次调用analyseModule函数,那我们进入到analyseModule看看这个函数完成了什么逻辑。
dependencies存储了当前Module依赖的内容,现在对其做深度优先遍历,之前有定义了一个记录引用的Map,值为parents,每处理一个Module,记录引用关系,大家要注意一下引用关系哦,a.js->b.js,那么b.js的parent是a.js。
nextExecIndex是定义在最外层的变量,递归体在nextExecIndex++语句的前面,也就是说,只有当递归处理到尽头的时候,nextExecIndex的值开始增加,然后依次退栈,nextExecIndex依次增加。
所以,还是回到之前我们举的例子:a.js->b.js->c.js,最终c.js的index就是1,b.js的index就是2,c.js的index就是3,将来排序的时候,c.js就可以被排到最前面了。
检测循环引用
循环引用在实际开发中是比较危险的操作,所以我们应当尽量减少编写循环引用的代码。
我们此时还没有聊到Chunk输出,不过现在可以给大家一些结论以示警:
当Module之间有循环依赖的时候,如果它们会被打包进入一个Chunk,这样的代码是没有问题的(回想一下是不是在上一篇文章中所看到的Rollup的fetchModule,多个函数形成一个调用环),当循环依赖的Module被划分至多个Chunk的时候,生成的产物在加载的时候就会报错。
Rollup有自己的机制可以处理循环引用,以下是它的处理流程,我们来分析一下这段代码的执行过程:
首先这个函数执行的条件是已确定有循环依赖的哟,而不是去探测有无循环依赖。
这个逻辑就是一个典型的循环链表的探测环,module就是链表的头结点,我们不断地向后迭代,当下一个节点执行的节点就是module的时候,我们读取到了一个完整的环,Rollup在处理的过程中,还往Module里面增加了一些节点信息,应该是为了后续处理的线索吧。
最后,我们看一下getCyclePath函数的执行条件:
第一个if条件成立的时候,说明当前模块可能是已经处理过的模块,如果处理过的话,就不用再处理了,当这两个if条件同时成立的时候,说明有一大堆的Module节点无法完成拓扑排序,因此需要记录依赖环。
生成Chunk
在上述的工作完成之后,Rollup就要开始生成Chunk了,即决定把什么内容打包到一个代码块中去。
这儿我们暂时先不配置自定义分包的策略,我们就先看一下Rollup是如何自动进行分包的。
对于我这个测试项目文件的依赖关系,大家如果不清楚的话,可以查看我的第二篇文章,在那篇文章中我画了一个依赖图。
这儿是利用位运算用来生成键,当键相同的时候,Module就可以被划分到同一个Chunk内了。至于这个位运算的算法原理是什么,我不是很懂,所以我请教了一下Chatgpt。
以下是Chatgpt给我的答案:
在上述代码中,位运算(bitwise operations)的主要用途是通过位标记(bitmask)的方式为一组依赖入口(dependentEntries)生成一个独特的“签名”(chunkSignature),从而将具有相同依赖入口集的模块归类到同一个输出分块中。
具体来说:
let chunkSignature = 0n;
for (const entryIndex of dependentEntries) {
chunkSignature |= 1n ;
}
这里的关键点在于:
使用 BigInt 和位移操作来创建位标记(Bitmask) :
1n