jQuery小文件合并生成大文件过程
负责合并小文件到一个大文件的代码位于build/tasks/build.js中。build.js主要做下面三件事:
- 判断要合并那些模块
- 根据依赖关系,确定各模块小文件的合并先后顺序
- 对每个小文件内部的代码进行调整
除了core和selector必须合并外,其他模块都可以剔除或顺带剔除(声明在Gruntfile.js的build任务配置中)。要剔除一个模块,比如sizzle和ajax/xhr,直接运行grunt custom:-sizzle,-ajax/xhr即可。关于jQuery的定制,可以参考jQuery Github主页关于Modules的说明。
依赖关系和顺序由requirejs的打包工具r.js计算,r.js同时会给小文件的define调用函数添加模块Id作为第一个参数。
在第3步中,build.js会把每个小文件的define函数调用的wrapper给删掉,只保留define最后一个参数factory函数的函数体,以便合并之后的大文件能脱离requirejs独立运行。不同的模块的有不同的具体处理方法。下面是小文件处理策略(convert函数):
1. 路径中包含var的小文件
比如,var/arr.js文件,
define(function() {
return [];
});
处理之后就变成:
var arr = [];
就是把return后面的内容(空数组[])赋值给以模块ID(var/arr)中var/之后字符串为名的变量(arr)。 这样arr变量对于所有依赖他的模块的factory代码来说,都是可见的了。jQuery的依赖var/xxx的小模块的define声明方式也很独特,以var/slice为例,它依赖var/arr:
define([
"./arr"
], function( arr ) {
return arr.slice;
});
var/arr模块的factory函数的依赖变量名是arr,正好是处理之后声明的变量名,等同于路径中var/之后的部分。合并后的大文件dist/jquery.js中头部有一坨var变量声明语句,都是这种方法处理生成的,每一个var xxx声明对应一个var/xxx模块。如下:
var arr = [];
var slice = arr.slice;
var concat = arr.concat;
var push = arr.push;
var indexOf = arr.indexOf;
var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;
var trim = "".trim;
var support = {};
2. sizzle.js处理方法
sizzle.js中代码大致如下
(function(window){
// sizzle source here
// EXPOSE
if ( typeof define === "function" && define.amd ) {
define(function() { return Sizzle; });
// Sizzle requires that there be a global window in Common-JS like environments
} else if ( typeof module !== "undefined" && module.exports ) {
module.exports = Sizzle;
} else {
window.Sizzle = Sizzle;
}
// EXPOSE
})(window);
处理之后的代码变成:
var Sizzle = (function(window){
//sizzle source here
return Sizzle;
})(window);
处理方法就是把闭包的返回值赋给Sizzle变量,并且把EXPOSE之间的内容用return Sizzle;替换掉。
其实sizzle小模块这样组织代码,即支持脱离requirejs单独运行,又可以方便地融入到jQuery模块化体系中,同时还很方便合并到大文件中。
3. 其他非jquery.js小文件的处理方法
删除末尾的return语句和exports.xxx=yyy语句,去掉开头和结尾的define warpper。例如ajax/parseJSON小文件:
define([
"../core"
], function( jQuery ) {
// Support: Android 2.3
// Workaround failure to string-cast null input
jQuery.parseJSON = function( data ) {
return JSON.parse( data + "" );
};
return jQuery.parseJSON;
});
上面是源文件,第2步处理的过程中会添加模块id作为define的第一个参数,如下:
define('ajax/parseJSON',[
"../core"
], function( jQuery ) {
// Support: Android 2.3
// Workaround failure to string-cast null input
jQuery.parseJSON = function( data ) {
return JSON.parse( data + "" );
};
return jQuery.parseJSON;
});
第3步删除最后的return jQuery.parseJSON;语句,删除开头的define和末尾的}),如下:
// Support: Android 2.3
// Workaround failure to string-cast null input
jQuery.parseJSON = function( data ) {
return JSON.parse( data + "" );
};
以上就是ajax/parseJSON小文件最终合并大大文件jQuery.js中的内容。
4. 入口文件jQuery.js的处理策略
jQuery.js文件作为入口模块,它依赖其他所有的模块,这样r.js就会把所有的小模块按顺序给组装在一起。前面提到了,可以剔除大部分的模块。这里就有一个玄机:被剔除的模块没有合并在大文件中,但是jQuery.js这个小模块依赖被剔除的模块。这样在浏览器端运行到jQuery.js这个小模块的时候,由于依赖的模块找不到,requirejs不就会报错吗?即便删除define wrapper,不需要requirejs了,但是在执行入口模块jQuery的factory代码的时候,如果引用了被踢出的模块exports的内容,运行时不就会出错吗?
这确实是个问题。看一下jQuery.js小模块的代码:
define([
"./core",
"./selector",
"./traversing",
"./callbacks",
"./deferred",
"./core/ready",
"./data",
"./queue",
"./queue/delay",
"./attributes",
"./event",
"./event/alias",
"./manipulation",
"./manipulation/_evalUrl",
"./wrap",
"./css",
"./css/hiddenVisibleSelectors",
"./serialize",
"./ajax",
"./ajax/xhr",
"./ajax/script",
"./ajax/jsonp",
"./ajax/load",
"./effects",
"./effects/animatedSelector",
"./offset",
"./dimensions",
"./deprecated",
"./exports/amd",
"./exports/global"
], function( jQuery ) {
return jQuery;
});
对于这个小模块,第2步中r.js只会给它加上模块ID,然后就进行第3步的处理,build.js对于jQuery.js小模块的处理方法是:直接删除前后的define wrapper。最后和合并到大文件时,这个模块就变成了:
return jQuery;
所以合并后的大文件中的最后一条语句就是上面的return jQuery; 由于这只是一个空的return语句,并没有引用它所声明的依赖模块对外所exports的任何变量。所以运行大文件到这一步时并不会报错。
总结
以上就是主要的合并过程了。可以看出,jquery的合并逻辑有很大的技巧性,这与模块的拆分方式有关。模块拆分直接导致合并逻辑复杂了。但好处也是明显的:各个模块可以单独加载运行,单独进行单元测试,同时支持根据不同的需求进行模块的删减定制。能实现这些益处,靠的是对模块内代码和文件目录的合理组织, 而代码和文件/目录的组织,正是前端工程的本质所在。
当然jQuery.js的模块拆分方案并不算完美,比如src目录下既有data.js又有data目录,按常理,data.js应该放在data目录里面,还有var目录,这个纯粹是为了方便合并才这样组织文件。
以上是以jQuery 2.1.0为例进行的分析,jQuery 1.x的合并逻辑与2.x的类似,只是小模块的内容有所不同。
补充
晚上和寒星在麦当劳吃饭的时候,讨论了下jquery小文件合并逻辑的不利之处:直接限制了代码和目录的组织方式,特别是ID中包含"var/"的模块(只能放在var目录下面),而且依赖这些模块的模块的factory函数的参数名称只能是这些模块ID中"var/"之后的部分。他提供的解决方案是,把小文件的factory函数都放在一个闭包里面立即执行,闭包的返回值赋给一个key为模块ID的公共变量hash中,立即执行函数传入的参数为key为依赖模块ID的公共变量hash的值。比如还是以刚才的模块var/arr和var/slice为例:
//模块var/arr小文件代码
define(function() {
return [];
});
//模块var/slice小文件代码,依赖模块var/arr
define([
"./arr"
], function( arr ) {
return arr.slice;
});
// -------------------分割线---------------------//
/* build/tasks/build.js的合并逻辑:
把return 的内容赋值给模块ID中"var/"之后的名称
*/
var arr = []; //模块var/arr
var slice = arr.slice; //模块var/slice
//--------------------分割线---------------------//
/* 寒星建议的合并逻辑:
把factory函数传入参数立即执行,并赋给外部公共变量
*/
var MODULES = {}; //只需要声明一次,在所有其他小模块之前
//var/arr小文件被合并之后的代码
MODULES["var/arr"] = (function(){
return [] ·
})();//没有依赖其他模块,不传入参数
//var/slice小文件被合并之后的代码,依赖模块var/arr
MODULES["var/slice"] = (function(arr){
return arr.slice;
})(MODULES["var/arr"]);//所依赖的模块依次通过MODULES[依赖的模块ID]传入
这种方法确实能免去var目录和factory函数参数名称的限制,而且能达到合并之后的大文件能脱离requirejs独立运行的目的。我的观点是:这样会造成合并之后的大文件dist/jquery.js代码阅读起来不自然!大文件中充斥着非jquery自身相关的逻辑!这些逻辑的作用就相当于被替换了的requirejs:模块加载器!
当然,寒星也理解我的观点。虽然jQuery提供了模块化的源代码供大家阅读,但是大部分人都是拿大文件来学习研究jQuery的!凡是未压缩混淆过的代码,都是面人的,要考虑阅读代码时的体验。
blog comments powered by Disqus