模块化如今应该已经成为了稍微复杂一点前端开发的标配了。在es6中,都已经支持了的模块化。 前端
以前的面试中,一直感受模块化AMD,CMD没有什么能够问的,不过昨天面试忽然想到一个题目:
对于一个AMD的模式下git
文件d.js
以下es6
define(function (require) { // ... 不少代码 require('a'); // ... 不少代码 require(['b'], function (b) {}); // ... 不少代码 require('c'); });
a.js
,b.js
,c.js
文件分别是何时加载的,如何加载的?github
题目不难面试
答案是a.js
和 c.js
是在加载完d.js
后就加载。b.js
是在执行到这一行时异步加载的。浏览器
具体分析:异步
我没有看过require.js
的源码,咱们使用的是esl.js
(也是一个AMD的模块加载器),可是他们的实现原理应该差很少。模块化
我从esl.js
的角度解读一下:性能
首先你们须要知道AMD里面一个同步加载和异步加载的概念。ui
从概念上面理解,同步就是当我执行到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
文件应该会处理以下
define( 'path/b', ['require', 'a', 'b'], function (require) { // ... 不少代码 require('a'); // ... 不少代码 require(['b'], function (b) {}); // ... 不少代码 require('c'); });
define方法会增长第一个和第二个参数
第一个参数是按照路径生成一个具名id
第二个参数是此文件所依赖的同步文件
这时当模块在解析这个b,js
文件时,发现若是存在第二个参数,就会直接解析所需依赖部分,而省去了正则这一步。
咱们正则这一步转换到了打包编译中去分析,这样就省掉了浏览器加载时去正则全部AMD文件这一步。
那么为何咱们不在开发环境中直接使用['require', 'a', 'b']
方式,我理解目的是为了提升开发便捷性,咱们不须要再增长一个require('*')
都在中括号内配置一次,一样删除时也不用去删掉配置。
由于这一步彻底能够在编译时处理。
不知道你们有没有看过编译后的代码和开发环境代码的区别,对于这个d.js
文件,编译后应该是:
define( 'path/a', ['require'], function (require) { // ... 不少代码 }); define( 'path/c', ['require'], function (require) { // ... 不少代码 }); define( 'path/b', ['require', 'a', 'b'], function (require) { // ... 不少代码 require('a'); // ... 不少代码 require(['b'], function (b) {}); // ... 不少代码 require('c'); });
上面可见,a.js
和c.js
这两个文件被合并到了d.js
中,全部文件都加上了具名id。并且这个id的生成规则是更具路径生成的。
而咱们异步加载的b.js
文件就没有被打包进来。这是由于咱们指望b.js
是懒加载的,当使用时在加载,这样也能达到按需加载的目的。