模块加载器的进化–并行加载

easy.js的模块加载器的详解(如果你没有阅读过,最好是先去阅读下,这样才能更好的理解这篇博文)中我详细的介绍过有关 easy.js 的加载器的实现。其加载和执行的顺序都要严格依赖队列一个一个的加载和执行,这种加载和执行方式就是串行。此文将介绍模块加载器并行加载的实现。

在讲解并行加载的实现原理之前,首先有必要对 JavaScript 文件的加载的执行有一个初步的了解。

浏览器的实现

JavaScript 在页面渲染时可能会对 DOM 元素进行修改,并且多个文件之间还会有依赖的关系,因此必须严格按照顺序依次执行,正是由于此种特性就势必对之后的页面资源的加载造成阻塞。但是请注意,这里说到的是按顺序执行

由于 JavaScript 阻塞的特性,也影响到浏览器对 JavaScript 文件的加载,在老版本浏览器中,加载完就执行,执行完再加载,这正是上面说到的串行加载。为了提升性能,在现代浏览器中,如新版本的 Firefox 和 Chrome,将加载改成了并行。并行加载允许一次性同时加载多个文件,在 HTTP1.1 中,多个文件并行加载只需要发起一个 TCP 连接数。试想下,一条流水线依次生产十个产品肯定要比十条流水线同时生产一个产品要慢得多。虽然在现代浏览器中可以实现并行加载 JavaScript,但是其执行顺序还是要按照顺序来执行的。

让加载可以并行

之前的 easy.js 的模块加载器的确是按照老版本浏览器串行加载和执行的思路来实现的,而最新版的模块加载器就是按照现代浏览器的并行加载,串行执行的思路来实现的。

模块加载器要加载一个 JavaScript 模块需要动态创建一个 script 标签来进行加载,这在前一篇文章中已经讲过。那么如何才能让动态创建的 script 标签并行的去加载呢?

浏览器之所以能并行加载多个文件是因为这些文件都是通过硬编码的形式写在页面中,这些标签本身就是在一起的。但是通过动态创建 script 标签的形式需要一个个去创建,如何去模拟浏览器的行为,确保这些动态创建的 script 标签也在一起呢?

我想到的就是利用一个文档碎片容器来存放动态创建的 script 标签,然后再一次性插入到 head 中,这样这些 script 标签就可以实现并行加载了。

这里不得不说我之前有点二的一个思路,当时是想将所有需要加载的模块包括依赖模块都一次性都插入到文档碎片中,这样可以尽可能的确保所有模块都是并行加载。如果你觉得这种办法挺好,那么你也跟着我一起犯二了。很关键性的一步就是,是否有依赖模块必须要等到该模块加载完才知道,所以不借助其他手段是不可能实现所有的模块都并行加载的。那么这里说到的并行加载其实还是依赖模块的并行加载,但能实现依赖模块的并行加载也算是一个很大的进步了。如果要实现所有模块都并行加载,就需要事先知道依赖了哪些模块,需要预先对模块进行解析,只能说目前而言,普通的模块加载器肯定是做不到的,但这也确实是一个不错的思路。

原来是使用一个队列来控制加载的顺序,加载完一个才加载下一个。但是现在改成并行的,队列依然有效,只是队列数组的其中某个元素可能就是一个由多个依赖模块组成的数组。多个依赖模块同时添加到 head 中,其加载是同时进行的,谁先加载完就先解析谁,如果依赖模块还有依赖模块,那么就让最先解析的这个模块的依赖模块再添加到队列的开始处。其实并行加载的控制队列已经没有顺序的概念了,没有了顺序的队列也可以不叫队列了。

让模块按顺序执行

并行的加载,加载完了就会执行该模块,这些都是异步的。但是对于 JavaScript 模块,执行的顺序是始终都不能变的。如果是普通的没有经过模块化封装的 JavaScript 文件,用这种动态添加 script 去实现并行加载的方式肯定是行不通的,并行加载会破坏执行顺序。

再回想下,加载完模块后,执行该模块实际上执行的是 define 这个全局函数,模块的内容 factory 依然是没有执行的,那么只要保证 factory 执行的顺序就相当于实现顺序执行模块。要保证顺序,还是用一个队列来控制 factory 的执行顺序。为了让 factory 尽快的执行完,每执行一次 define,都会去检查该队列的第一个模块的所有依赖模块的状态是否为已执行完,如果已执行完则执行该模块的 factroy。

实例讲解

还是用一个具体的实例来介绍下整个流程。

有 a、b、c、d 4个模块,a 依赖了 c、d 模块。

模块 a:

// a.js
define( 'a', ['c', 'd'], function(){
    return 'module a is done, deps[' + c + ', ' + d + ']';
});

模块 b:

// b.js
define( 'b', function(){
    return 'module b is done';
});

模块 c:

// c.js
define( 'c', function(){
    return 'module c';
});

模块 d:

// d.js
define( 'd', function(){
    return 'module d';
});

同时加载 a、b 模块:

E.use( ['a', 'b'], function( a, b ){
    console.log( a + ', ' + b );
});

第一步:调用 use 方法,解析 a、b 模块,解析出模块名和模块 url,将模块名和模块 url 都各自添加到一个数组中,创建加载队列,将模块名和模块 url 组成的数组都添加到加载队列中。

第二步:依据 a、b 模块的 url,创建两个 script 元素,将两个 script 元素都添加到一个文档碎片容器中,最后将文档碎片添加到 head 中。此时已经开始同时加载 a、b 模块了。

第三步:a、b 模块并不知道哪一个先加载完,但是没关系,这里假设 a 先加载完,那么执行 a 的 define 函数时,解析出 a 的依赖 c、d。因为 a 有依赖,那么此时还不能执行 a 的 factroy,创建一个 factory 的队列,将 a 的 factory 添加到该队列中。同样,解析 c、d 的模块名和模块 url 都存放到一个数组中,将数组添加到加载队列中。此时 b 模块也加载完了,执行其 define 时,没有解析到有依赖,那么直接执行 b 的 factory。

第四步:c、d 模块执行第二步,加载完后,都会执行 define,没有依赖就直接执行其 factory,然后尝试去执行 factory 队列中 a 的 factory,只有等到 c、d 的 factory 都执行完了才能执行 a 的 factory。

第五步:执行完 a 的 factory,此时加载队列有依赖队列都是空的,那么可以执行 use 方法的回调函数了,到此时 a、b 模块全部加载和执行完。

在上面的加载过程中,a、b 是并行一组进行加载,c、d 是并行一组进行加载。这里将上述实例的代码做成了 Demo,有兴趣的猛击这里查看并行加载的Demo

如果你有兴趣研究源码,可以查看下面的链接。

https://github.com/chenmnkken/easyjs/blob/master/core/src/easy.js

原载于:雨夜带刀’s Blog
本文链接:https://blog.yiguochen.com/easyjs-parallel-moduleloader.html
如需转载请以链接形式注明原载或原文地址。

“模块加载器的进化–并行加载”目前一条评论