模块化现在应该已经成为了稍微复杂一点前端开发的标配了。在es6中,都已经支持了的模块化。
之前的面试中,一直感觉模块化AMD,CMD没有什么可以问的,不过昨天面试突然想到一个题目:
对于一个AMD的模式下
文件d.js
如下
1 | define(function (require) { |
a.js
,b.js
,c.js
文件分别是什么时候加载的,如何加载的?
题目不难
答案是a.js
和 c.js
是在加载完d.js
后就加载。b.js
是在执行到这一行时异步加载的。
具体分析:
我没有看过require.js
的源码,我们使用的是esl.js
(也是一个AMD的模块加载器),但是他们的实现原理应该差不多。
我从esl.js
的角度解读一下:
同步加载,异步加载
首先大家需要知道AMD里面一个同步加载和异步加载的概念。
从概念上面理解,同步就是当我执行到require('a');
时,我需要同步的执行a.js
里面的内容,也就是需要在执行到这句话时a.js
必须已经加载好了,这样才能到达同步。
而对于 require(['b'], function (b) {});
,我执行到这一步时,是异步的发出请求,然后异步等待b.js
的返回+执行。
同步加载的实现原理
我们从概念上面理解的同步加载的原理,现在看看esl.js
的实践。
这里面需要处理两个核心步骤
- 执行到
require('a');
时,a.js
必须已经加载好了; a.js
文件里面的所有require('*')
,也都必须加载好了,保证在执行a.js
时,所有a.js
依赖的同步文件都能同步执行;
对于第一步的实现,大概原理是这样的,在加载好了d.js
后,会正则匹配一次文件里面的同步依赖require('*');
,例如匹配出了 a.js
和c.js
,然后继续加载a.js
和c.js
。
对于第二部,其实就是一个递归处理,直到没有下一步的依赖为止。
同步加载另外一种处理方法
上面有一部正则逻辑,可见如果使用这种方式,在执行代码前,js需要全部正则一次所有模块化代码的。这样性能是不是有一个无谓的耗损。
那么我们一般怎么处理了?
大家一般都了解过打包编译,例如在使用Requirejs
时,线上环境的代码会经过r.js
处理一次。
那么d.js
文件应该会处理如下
1 | define( |
define方法会增加第一个和第二个参数
第一个参数是按照路径生成一个具名id
第二个参数是此文件所依赖的同步文件
这时当模块在解析这个b,js
文件时,发现如果存在第二个参数,就会直接解析所需依赖部分,而省去了正则这一步。
我们正则这一步转换到了打包编译中去分析,这样就省掉了浏览器加载时去正则所有AMD文件这一步。
那么为什么我们不在开发环境中直接使用['require', 'a', 'b']
方式,我理解目的是为了提高开发便捷性,我们不需要再增加一个require('*')
都在中括号内配置一次,同样删除时也不用去删掉配置。
因为这一步完全可以在编译时处理。
打包编译的延生
不知道大家有没有看过编译后的代码和开发环境代码的区别,对于这个b.js
文件,编译后应该是:
1 | define( |
上面可见,a.js
和c.js
这两个文件被合并到了d.js
中,所有文件都加上了具名id。而且这个id的生成规则是更具路径生成的。
而我们异步加载的b.js
文件就没有被打包进来。这是因为我们期望b.js
是懒加载的,当使用时在加载,这样也能达到按需加载的目的。