【写在前面】
最近萌生了写一个“源码系列”的想法,一则让自己静心下来做点事情,二是借由这个过程,在积累专业知识的同时,也把整个技术框架系统的扎起来。
之所以选择了一个工具库作为切入点,也并没有什么特殊的原因,只是因为它的复杂度适中,源码也只有不到2000行,容易下手而已。嗯,希望这个系列可以不断丰满下去。
毕竟,分享是一件让人快乐的事情。
【看这里】
阅读源码的过程中,如果有任何不容易理解的地方,可以和使用文档[underscorejs.org/]对比来看,会容易理解…
一、我们先来看一下UnderScore的整体结构
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define('underscore', factory) :
(global = global || self, (function () {
var current = global._;
var exports = global._ = factory();
exports.noConflict = function () { global._ = current; return exports; };
}()));
}(this, (function () {
...
})));
//# sourceMappingURL=underscore.js.map
1、最外层是一个立即执行函数,接收两个参数
// wrapper部分
(function (global, factory)){}(A,B))
A=this, 对应形式参数global,在浏览器侧对应window对象,在服务端对应global对象;B=(function(){}), 是一个工厂方法,对应形式参数factory。
2、函数内部2~8行,定义了不同模块规范下,该模块的导出方式。
// 判断是否是commonJs
typeof exports === 'object' && typeof module !== 'undefined'
// 判断是否是amd
typeof define === 'function' && define.amd
// 其它情况
(global = global || self, (function () {
var current = global._;
var exports = global._ = factory();
exports.noConflict = function () { global._ = current; return exports; };
}())
(1)前两行分别判断了运行环境是否为commonJS或者AMD,关于各种规范的区别和判断语句的原理,参看下面的【扩展阅读】章节。
(2)剩下的部分是一个最外层用()包裹的代码块,其中包含多条语句,语句间直接使用逗号间隔。也就是说括号内部的部分都会执行。立即执行函数内部,global.其实是在全局对象上声明了一个属性, 属性值即为factory方法返回的结果,也就是内部定义的方法集合。所以我们可以通过global._访问underscore内部的任意方法。
(3)exports.noConflict方法是将global._属性重置了,只返回了导出的方法集合,是为了应对多个库挂载冲突的问题。比如有个DH模块也把导出的方法挂载到了global._上,此时就和underscore产生了冲突。可以通过以下方法解决冲突:
global.__ = underscore.noConflict(); // 将underscore挂载到新的字段上
3、最后一行,用于指定sourceMap位置。可以关注webpack的打包方式,不同的的配置,产生的sourceMap方式不同。
二、再来看一下方法是怎样组织导出的
// Named Exports
var allExports = {
__proto__: null,
VERSION: VERSION,
restArguments: restArguments,
isObject: isObject,
isNull: isNull,
...
mixin: mixin,
'default': _
};
// Default Export
// Add all of the Underscore functions to the wrapper object.
var _$1 = mixin(allExports);
// Legacy Node.js API.
_$1._ = _$1;
return _$1;
allExports包含了需要导出的全部方法,以k:v的形式指定了字段命名和映射关系。所有的方法经过mixin方法处理之后,返回一个对象,该对象的_属性指向自己,并作为factory方法的结果返回出去。
// Start chaining a wrapped Underscore object.
function chain(obj) {
var instance = _(obj);
instance._chain = true;
return instance;
}
// Helper function to continue chaining intermediate results.
function chainResult(instance, obj) {
return instance._chain ? _(obj).chain() : obj;
}
// Add your own custom functions to the Underscore object.
function mixin(obj) {
each(functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return chainResult(this, func.apply(_, args));
};
});
return _;
}
chain方法将传入的对象包装为链式调用的对象,并设置一个_chain的标志位,表示该对象已经被包装过。该方法是为了支持链式调用的场景。链式调用的原理也是比较简单的,一般是返回this本身, 或该对象的实例。
_.chain().each().map()
mixin方法是Underscore提供的可以扩展自定义方法的接口。很多工具库都提供了扩展自身的接口,类似JQuery的extend等。扩展的方法也基本都是mixin模式,即将一个对象的方法属性拷贝到另一个对象上。
这里,mixin方法做了几件事情。第一,将obj的方法拷贝到_对象上,同名的会被覆盖。第二,将obj的方法映射到_的原型对象上。之所以说是映射,并非是完全的拷贝,是因为设置在原型对象上的,是加了chainResult处理步骤的方法。chainResult其实就是提供了一个对链式调用的判断,如果当前对象被链式包装过,就继续调用chain方法进行链式调用的包装,否则返回当前对象。
// If Underscore is called as a function, it returns a wrapped object that can
// be used OO-style. This wrapper holds altered versions of all functions added
// through `_.mixin`. Wrapped objects may be chained.
function _(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
}
chain方法中的第一句,将obj传入了_方法进行了处理。_方法中,通过new的方式,生成了一个_的实例对象,而_的原型对象上又映射了所有的方法集合。所以返回的新对象可以通过链式调用的方式来进行方法调用。
// Add all of the Underscore functions to the wrapper object.
var _$1 = mixin(allExports);
// Legacy Node.js API.
_$1._ = _$1;
return _$1;
Underscore自己也是使用了mixin的方法,将所有内部方法都导出暴露在_对象和其原型上。
总结:Underscore的整体结构还是比较简单的,我们将在下一篇《UNDERSCORE.js 源码解析(二)》中来看一些工具库内部有意思的片段。
【扩展阅读】
一、AMD/CMD/CommonJS/UMD的区别
1、AMD(Asynchronous Module Definition,异步模块定义) [1]
define([module-name?], [array-of-dependencies?], [module-factory-or-object]);
只有一个define函数的范式定义。前两个参数是可选的,分别标识了模块名称和依赖关系。最后一个参数是必选项,可以是一个具体的对象,或者是一个函数方法。它的使用方式如下:
define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
exports.verb = function() {
return beta.verb();
//Or:
return require("beta").verb();
}
});
同时,规范还定义了每个遵循AMD规范的模块实现都应该提供了一个属性字段'amd',用来明确标识该模块是基于AMD规范实现的。因此,在一些工具库的实现中,我们常常看到使用这样的语句来判断当前环境是否支持AMD规范:
typeof define === 'function' && define.amd ?
当define函数中,没传入moudle-name参数时,该模块为一个匿名模块。此时,默认使用文件名作为模块名称。以大家较为熟悉的RequireJS为例,引入一个模块的方式如下:
// Consider "foo" and "bar" are two external modules
// In this example, the "exports" from the two modules
// loaded are passed as function arguments to the
// callback (foo and bar) so that they can similarly be accessed
require(["foo", "bar"], function ( foo, bar ) {
// rest of your code here
foo.doSomething();
});
require第一个参数声明了依赖的其它模块。当然,也可以动态的加载依赖:
define(function ( require ) {
var isReady = false, foobar;
// note the inline require within our module definition
require(["foo", "bar"], function ( foo, bar ) {
isReady = true;
foobar = foo() + bar();
});
// we can still return a module
return {
isReady: isReady,
foobar: foobar
};
});
2、CMD(Common Module Definition, 通用模块定义)[4]
CMD是国内前端大佬玉伯在推广Sea.js时,提出的一种模块规范。AMD的实践者RequireJS曾经只支持依赖前置,CMD则支持依赖就近,这曾经被认为是AMD和CMD最明显的差异。但后来RequireJS也支持了依赖就近的写法。关于其它的一些对比,可以参考玉伯本人关于这个问题的回答[5]。CMD规范在后来的前端技术发展中逐渐没落,其具体内容参见[4],此处不再赘述。
3、CommonJS
正如CommonJS官方站点首页的Slogan(javascript: not just for browsers any more!)写到的那样,自从CommonJS和NodeJS诞生之后,JS在服务端和本地应用编程中开始大放异彩。
NodeJS作为基于CommonJS的优秀实现,已经成为服务端JS框架的事实标准。这里以它为例,简单介绍一下CommonJS的内容。NodeJS中定义了一个全局变量global,类似BOM中的window对象。在global上挂载了多个字段属性。其中module、require、exports与模块化相关,可以直接全局访问。
通常,module.exports用于制定模块导出的内容,可以使用require来进行访问。exports是对module.exports的更简短的引用形式。开发者可以在其上挂载任意方法和属性,但不可以对其进行赋值。一旦赋值,exports和module.exports之间的联系也就被人为阻断了。所以可以通过判断exports和module对象是否存在,来判断是否符合CommonJS规范:
typeof exports === 'object' && typeof module !== 'undefined'
4、UMD(Universal Module Definition,通用模块定义)
UMD[8]是一种兼容AMD和CommonJS的方式,通过检测运行环境对不同规范的支持,来使用不同的方式导出模块内容。正如文章开头时候展示的片段,Underscore.js以及许多工具库都是使用UMD的方式对外导出的。
二、关于Source Map
Source Map用来指定编译压缩之后的文件和源文件的对应关系,在开发调试和错误trace中有很大的帮助。一般可以使用两种方式启用source map:
// 1、在文件的最后一行,插入如下语句,一般是编译器工具自动插入的
//# sourceMappingURL=/path/xxxx.js.map
// 2、在压缩文件的请求响应中添加header字段
X-SourceMap: /path/to/script.js.map
在Webpack、UglifyJS等打包编译工具中,都暴露了一些选项用于调整source map的生成规则,来平衡压缩时间和调试需求。具体见[11]。
WX公众号同步更新
【相关链接】
[1] AMD规范定义: github.com/amdjs/amdjs…
[2] AMD解读: www.w3cschool.cn/zobyhd/fcyo…
[3] SeaJS: github.com/seajs/seajs
[4] CMD规范定义: github.com/cmdjs/speci…
[5] AMD和CMD的区别:github.com/seajs/seajs…
[6] CommonJS:www.commonjs.org/
[7] CommonJS Modules/1.1: wiki.commonjs.org/wiki/Module…
[8] UMD规范定义:github.com/umdjs/umd
[9] Source Map介绍:blog.teamtreehouse.com/introductio…
[10] Source Map MDN: developer.mozilla.org/en-US/docs/…
[11] Webpack Source Map: www.webpackjs.com/configurati…
[12] Function MDN: developer.mozilla.org/en-US/docs/…