prototype.js 源码解读
prototype 1.3.1 版本和之前的 1.2.0 版本有了不少改进,并增加了新的功能:
1. 增加了事件注册管理
2. 增加了空间定位的常用函数
3. 改善了 xmlhttp 的封装
4. 移除了 Effect.js,交给 Rico 或者 script.aculo.us 这些扩展库类实现。
5. bug 修复
代码:
/**
* 定义一个全局对象, 属性 Version 在发布的时候会替换为当前版本号
*/
var Prototype = {
Version: '1.3.1',
// 一个空方法,其后的代码常会用到,先前的版本该方法被定义于 Ajax 类中。
emptyFunction: function() {}
}
/**
* 创建一种类型,注意其属性 create 是一个方法,返回一个构造函数。
* 一般使用如下
* var X = Class.create(); 返回一个类型,类似于 java 的一个Class实例。
* 要使用 X 类型,需继续用 new X()来获取一个实例,如同 java 的 Class.newInstance()方法。
*
* 返回的构造函数会执行名为 initialize 的方法, initialize 是 Ruby 对象的构造器方法名字。
* 此时initialize方法还没有定义,其后的代码中创建新类型时会建立相应的同名方法。
*
*/
var Class = {
create: function() {
return function() {
this.initialize.apply(this, arguments);
}
}
}
/**
* 创建一个对象,从变量名来思考,本意也许是定义一个抽象类,以后创建新对象都 extend 它。
* 但从其后代码的应用来看, Abstract 更多是为了保持命名空间清晰的考虑。
* 也就是说,我们可以给 Abstract 这个对象实例添加新的对象定义。
*/
var Abstract = new Object();
Object.extend = function(destination, source) {
for (property in source) {
destination[property] = source[property];
}
return destination;
}
/**
* 获取参数对象的所有属性和方法,有点象多重继承。但是这种继承是动态获得的。
* 如:
* var a = new ObjectA(), b = new ObjectB();
* var c = a.extend(b);
* 此时 c 对象同时拥有 a 和 b 对象的属性和方法。但是与多重继承不同的是,c instanceof ObjectB 将返回false。
*
* 旧版本的该方法定义如下:
* Object.prototype.extend = function(object) {
* for (property in object) {
* this[property] = object[property];
* }
* return this;
* }
*
* 新的形式新定义了一个静态方法 Object.extend,这样做的目的大概是为了使代码更为清晰
*/
Object.prototype.extend = function(object) {
return Object.extend.apply(this, [this, object]);
}
/**
* 这个方法很有趣,它封装一个javascript函数对象,返回一个新函数对象,新函数对象的主体和原对象相同,但是bind()方法参数将被用作当前对象的对象。
* 也就是说新函数中的 this 引用被改变为参数提供的对象。
* 比如:
*
*
* .................
*
* 那么,调用aaa.showValue 将返回"aaa", 但调用aaa.showValue2 将返回"bbb"。
*
* apply 是ie5.5后才出现的新方法(Netscape好像很早就支持了)。
* 该方法更多的资料参考MSDN [url]http://msdn.microsoft.com/library/en-us/script56/html/js56jsmthApply.asp[/url]
* 阅读其后的代码就会发现,bind 被应用的很广泛,该方法和 Object.prototype.extend 一样是 Prototype 的核心。
* 还有一个 call 方法,应用起来和 apply 类似。可以一起研究下。
*/
Function.prototype.bind = function(object) {
var __method = this;
return function() {
__method.apply(object, arguments);
}
}
/**
* 和bind一样,不过这个方法一般用做html控件对象的事件处理。所以要传递event对象
* 注意这时候,用到了 Function.call。它与 Function.apply 的不同好像仅仅是对参数形式的定义。
*/
Function.prototype.bindAsEventListener = function(object) {
var __method = this;
return function(event) {
__method.call(object, event || window.event);
}
}
/**
* 将整数形式RGB颜色值转换为HEX形式
*/
Number.prototype.toColorPart = function() {
var digits = this.toString(16);
if (this < 16) return '0' + digits;
return digits;
}
/**
* 典型 Ruby 风格的函数,将参数中的方法逐个调用,返回第一个成功执行的方法的返回值
*/
var Try = {
these: function() {
var returnValue;
for (var i = 0; i < arguments.length; i++) {
var lambda = arguments[i];
try {
returnValue = lambda();
break;
} catch (e) {}
}
return returnValue;
}
}
/*--------------------------------------------------------------------------*/
/**
* 一个设计精巧的定时执行器
* 首先由 Class.create() 创建一个 PeriodicalExecuter 类型,
* 然后用对象直接量的语法形式设置原型。
*
* 需要特别说明的是 rgisterCallback 方法,它调用上面定义的函数原型方法bind, 并传递自己为参数。
* 之所以这样做,是因为 setTimeout 默认总以 window 对象为当前对象,也就是说,如果 registerCallback 方法定义如下的话:
* registerCallback: function() {
* setTimeout(this.onTimerEvent, this.frequency * 1000);
* }
* 那么,this.onTimeoutEvent 方法执行失败,因为它无法访问 this.currentlyExecuting 属性。
* 而使用了bind以后,该方法才能正确的找到this,也就是PeriodicalExecuter的当前实例。
*/
var PeriodicalExecuter = Class.create();
PeriodicalExecuter.prototype = {
initialize: function(callback, frequency) {
this.callback = callback;
this.frequency = frequency;
this.currentlyExecuting = false;
this.registerCallback();
},
registerCallback: function() {
setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
},
onTimerEvent: function() {
if (!this.currentlyExecuting) {
try {
this.currentlyExecuting = true;
this.callback();
} finally {
this.currentlyExecuting = false;
}
}
}
}
/*--------------------------------------------------------------------------*/
/**
* 这个函数就 Ruby 了。我觉得它的作用主要有两个
* 1. 大概是 document.getElementById(id) 的最简化调用。
* 比如:$("aaa") 将返回 aaa 对象
* 2. 得到对象数组
* 比如: $("aaa","bbb") 返回一个包括id为"aaa"和"bbb"两个input控件对象的数组。
*/
function $() {
var elements = new Array();
for (var i = 0; i < arguments.length; i++) {
var element = arguments[i];
if (typeof element == 'string')
element = document.getElementById(element);
if (arguments.length == 1)
return element;
elements.push(element);
}
return elements;
}
/**
* 为兼容旧版本的浏览器增加 Array 的 push 方法。
*/
if (!Array.prototype.push) {
Array.prototype.push = function() {
var startLength = this.length;
for (var i = 0; i < arguments.length; i++)
this[startLength + i] = arguments[i];
return this.length;
}
}
/**
* 为兼容旧版本的浏览器增加 Function 的 apply 方法。
*/
if (!Function.prototype.apply) {
// Based on code from [url]http://www.youngpup.net/[/url]
Function.prototype.apply = function(object, parameters) {
var parameterStrings = new Array();
if (!object) object = window;
if (!parameters) parameters = new Array();
for (var i = 0; i < parameters.length; i++)
parameterStrings[i] = 'parameters[' + i + ']';
object.__apply__ = this;
var result = eval('object.__apply__(' +
parameterStrings.join(', ') + ')');
object.__apply__ = null;
return result;
}
}
/**
* 扩展 javascript 内置的 String 对象
*/
String.prototype.extend({
/**
* 去掉字符串中的标签
*/
stripTags: function() {
return this.replace(/<\/?[^>]+>/gi, '');
},
/**
* 这个方法很常见,通常的实现都是用正则表达式替换特殊字符为html规范定义的命名实体或者十进制编码,比如:
* string.replace(/&/g, "&").replace(//g, ">");
* 而这里的实现借用浏览器自身的内部替换,确实巧妙。
*/
escapeHTML: function() {
var div = document.createElement('div');
var text = document.createTextNode(this);
div.appendChild(text);
return div.innerHTML;
},
/**
* 同上
*/
unescapeHTML: function() {
var div = document.createElement('div');
div.innerHTML = this.stripTags();
return div.childNodes[0].nodeValue;
}
});
/**
* 定义 Ajax 对象, 静态方法 getTransport 方法返回一个 XMLHttp 对象
*/
var Ajax = {
getTransport: function() {
return Try.these(
function() {return new ActiveXObject('Msxml2.XMLHTTP')},
function() {return new ActiveXObject('Microsoft.XMLHTTP')},
function() {return new XMLHttpRequest()}
) || false;
}
}
/**
* 我以为此时的Ajax对象起到命名空间的作用。
* Ajax.Base 声明为一个基础对象类型
* 注意 Ajax.Base 并没有使用 Class.create() 的方式来创建,我想是因为作者并不希望 Ajax.Base 被库使用者实例化。
* 作者在其他对象类型的声明中,将会继承于它。
* 就好像 java 中的私有抽象类
*/
Ajax.Base = function() {};
Ajax.Base.prototype = {
/**
* extend (见上) 的用法真是让人耳目一新
* options 首先设置默认属性,然后再 extend 参数对象,那么参数对象中也有同名的属性,那么就覆盖默认属性值。
* 想想如果我写这样的实现,应该类似如下:
setOptions: function(options) {
this.options.methed = options.methed? options.methed : 'post';
..........
}
我想很多时候,java 限制了 js 的创意。
*/
setOptions: function(options) {
this.options = {
method: 'post',
asynchronous: true,
parameters: ''
}.extend(options || {});
},
/**
* 如果 xmlhttp 调用返回正确的HTTP状态值,函数返回ture, 反之false。
* xmlhttp 的 readyState 属性不足以准确判断 xmlhttp 远程调用成功,该方法是readyState判断的一个前提条件
*/
responseIsSuccess: function() {
return this.transport.status == undefined
|| this.transport.status == 0
|| (this.transport.status >= 200 && this.transport.status < 300);
},
/**
* 如果 xmlhttp 调用返回错误的HTTP状态值,函数返回ture, 反之false。
*/
responseIsFailure: function() {
return !this.responseIsSuccess();
}
}
/**
* Ajax.Request 封装 XmlHttp
*/
Ajax.Request = Class.create();
/**
* 定义四种事件(状态), 参考[url]http://msdn.microsoft.com/workshop/author/dhtml/reference/properties/readystate_1.asp[/url]
*/
Ajax.Request.Events =
['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
/**
* 相比先前的版本,对于 xmlhttp 的调用和返回值处理分离得更为清晰
*/
Ajax.Request.prototype = (new Ajax.Base()).extend({
initialize: function(url, options) {
this.transport = Ajax.getTransport();
this.setOptions(options);
this.request(url);
},
/**
* 新增加 request 方法封装 xmlhttp 的调用过程。
*/
request: function(url) {
var parameters = this.options.parameters || '';
if (parameters.length > 0) parameters += '&_=';
try {
if (this.options.method == 'get')
url += '?' + parameters;
this.transport.open(this.options.method, url,
this.options.asynchronous);
if (this.options.asynchronous) {
this.transport.onreadystatechange = this.onStateChange.bind(this);
setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10);
}
this.setRequestHeaders();
var body = this.options.postBody ? this.options.postBody : parameters;
this.transport.send(this.options.method == 'post' ? body : null);
} catch (e) {
}
},
/**
* 新增加的 setRequestHeaders 方法允许添加自定义的http header
*/
setRequestHeaders: function() {
var requestHeaders =
['X-Requested-With', 'XMLHttpRequest',
'X-Prototype-Version', Prototype.Version];
if (this.options.method == 'post') {
requestHeaders.push('Content-type',
'application/x-www-form-urlencoded');
/* Force "Connection: close" for Mozilla browsers to work around
* a bug where XMLHttpReqeuest sends an incorrect Content-length
* header. See Mozilla Bugzilla #246651.
*/
if (this.transport.overrideMimeType)
requestHeaders.push('Connection', 'close');
}
/**
* 其后的 apply 方法的调用有些奇技淫巧的意味
* 从上下文中我们可以分析出 this.options.requestHeaders 是调用者自定义的http header数组。
* requestHeaders 也是一个数组,将一个数组中的元素逐个添加到另一个元素中,直接调用
* requestHeaders.push(this.options.requestHeaders)
* 是不行的,因为该调用导致 this.options.requestHeaders 整个数组作为一个元素添加到 requestHeaders中。
* javascript的Array对象还提供一个concat 的方法表面上满足要求,但是concat实际上是创建一个新数组,将两个数组的元素添加到新数组中。
* 所以,下面的代码也可以替换为
* requestHeaders = requestHeaders.concat(this.options.requestHeaders);
* 很显然,作者不喜欢这样的代码方式
* 而 apply 方法的语法 apply([thisObj[,argArray]]) 本身就要求第二个参数是一个数组或者arguments对象。
* 所以巧妙的实现了 concat 函数的作用。
* 令人拍案叫绝啊!
*/
if (this.options.requestHeaders)
requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
for (var i = 0; i < requestHeaders.length; i += 2)
this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
},
onStateChange: function() {
var readyState = this.transport.readyState;
/**
* 如果不是 Loading 状态,就调用回调函数
*/
if (readyState != 1)
this.respondToReadyState(this.transport.readyState);
},
/**
* 回调函数定义在 this.options 属性中,比如:
var option = {
onLoaded : function(req) {...};
......
}
new Ajax.Request(url, option);
*/
respondToReadyState: function(readyState) {
var event = Ajax.Request.Events[readyState];
/**
* 新增的回调函数处理,调用者还可以在options中定义 on200, onSuccess 这样的回调函数
* 在 readyState 为完成状态的时候调用
*/
if (event == 'Complete')
(this.options['on' + this.transport.status]
|| this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
|| Prototype.emptyFunction)(this.transport);
(this.options['on' + event] || Prototype.emptyFunction)(this.transport);
/* Avoid memory leak in MSIE: clean up the oncomplete event handler */
if (event == 'Complete')
this.transport.onreadystatechange = Prototype.emptyFunction;
}
});
/**
* Ajax.Updater 用于绑定一个html元素与 XmlHttp调用的返回值。类似与 buffalo 的 bind。
* 如果 options 中有 insertion(见后) 对象的话, insertion 能提供更多的插入控制。
*/
Ajax.Updater = Class.create();
Ajax.Updater.ScriptFragment = '(?:
Ajax.Updater.prototype.extend(Ajax.Request.prototype).extend({
initialize: function(container, url, options) {
/**
* containers 就是被绑定的 html 对象,xmlhttp的返回值被赋给该对象的 innerHTML 属性。
* 相比新版本,containers 根据container参数定义 success 和 failure 引用,如果它们被定义的话,根据xmlhttp调用是否成功来选择
* 更新对象,假想调用可能如下:
* var c = {success: $("successDiv"), failure: $("failureDiv")};
* new Ajax.Updater(c, url, options);
* 那么调用成功则 successDiv 显示成功信息或者数据,反之 failureDiv 显示错误信息
*/
this.containers = {
success: container.success ? $(container.success) : $(container),
failure: container.failure ? $(container.failure) :
(container.success ? null : $(container))
}
this.transport = Ajax.getTransport();
this.setOptions(options);
var onComplete = this.options.onComplete || Prototype.emptyFunction;
this.options.onComplete = (function() {
this.updateContent();
onComplete(this.transport);
}).bind(this);
this.request(url);
},
updateContent: function() {
var receiver = this.responseIsSuccess() ?
this.containers.success : this.containers.failure;
var match = new RegExp(Ajax.Updater.ScriptFragment, 'img');
var response = this.transport.responseText.replace(match, '');
var scripts = this.transport.responseText.match(match);
if (receiver) {
if (this.options.insertion) {
new this.options.insertion(receiver, response);
} else {
receiver.innerHTML = response;
}
}
if (this.responseIsSuccess()) {
if (this.onComplete)
setTimeout((function() {this.onComplete(
this.transport)}).bind(this), 10);
}
/**
* 如果调用者在传入的options参数中定义 evalScripts=true,同时xmlhttp返回值的html中包含 文本。
* 没有g, scripts[i].match(match)[1] 匹配的就是