jquery1.43源码分析之工具方法

2010-11-21 09:50

jquery1.43源码分析之工具方法

by

at 2010-11-21 01:50:38

original http://www.javaeye.com/topic/818329

这个部分是jquery一些常用的工具方法. 包括为jquery对象扩展了一些数组里的方法.一些测试方法,函数代理和浏览器的特性检测.

数组和对象操作.这部分的很多方法都已经成为javascript1.6的标准.

这部分包括一些原型函数,静态函数,内部函数.
原型函数主要通过api暴露给外界. 静态方法主要包含了原型方法的具体逻辑实现. 内部函数主要供内部调用.
prototype上的方法一般设计得比较简单, 主要充当一个控制层的作用. 而具体的实现逻辑, 大多数都放在了底层的静态方法中. 这样将来版本升级的时候, 一般只要修改静态方法就可以了,跟api紧密耦合的prototype方法不会受到太多牵连.


这部分其中有一些方法已经在jquery1.43源码分析之核心部分写过了, 看看另外一些方法的实现.


jQuery.isArray

isArray: function( obj ) {
        return toString.call(obj) === "[object Array]";
    }

判断对象是不是Array类型.这个简单的需求经历了漫长的演变过程.
一般我们判断对象类型, 会想到下面几种方式.
1 typeof    typeof  只能判断大概区分是不是对象类型, 不论是Array还是Date还是null,还是通过自定义构造函数生成的对象.返回的都是object
2 instanceof和constructor  都非常容易在原型继承中出现问题,
比如
var A = function(){

} A.prototype = new Array;

var a = new A;

alert (a instanceof Array) //true


而且在不同的iframe中, 判断也不准确, 因为不同的iframe不共享原型链. instanceof和constructor自然也会失效.
3 后来就有了流行一时的鸭式变型. 如果一只鸭子, 它会呱呱叫, 也会像鸭子一样走路. 那么就认为它就是鸭子.
isArray: function(object) {   
return object != null && typeof object == "object" &&  
 'splice' in object && 'join' in object;   
}  


不过也许有那么一只聪明的鸡学会了呱呱叫, 也学会了像鸭子一样走路.
比如 var a = {}; a.splice = 1; a.join = 2;

前面的方法显然都不完美, 直到有个人发现了Object.prototype.toString.call(obj) === "[object Array]", 世界才变美好.其实就是利用toString方法来得到一个包含这个对象内部属性的字符串,这个字符串就包含了此对象构造器的信息.



jQuery.prototype.each

顾名思义, each方法是对数组或者对象中的每个元素都做一些类似的操作. 我们在看源码之前先自己实现一个Array.prototype.each.
Array.prototype.each = function(fn){
    for (var i = 0, l = this.length; i < l; i++){
        fn(this[i]);  //当前元素作为回调函数的参数.
    }
};

[1, 2, 3].each(function(i){ alert (i); //i 就是上面的 this[i] })


很简陋, 然后想想它有哪些不足.
1, 在回调函数里我不知道当前循环到了第几个元素.
2, 回调函数里的this指向了window, 这个没任何意义.

所以现在来稍微改一下.
Array.prototype.each = function(fn){
    for (var i = 0, l = this.length; i < l; i++){
        fn.call(this[i], i, this[i]);
    }
};

[1, 2, 3].each(function(i, n){ alert ([this, i, n]); })



现在在回调函数里可以取到3个值, this指向当前元素. i表示循环到了第几个. n也表示当前元素. 当然你也可以把this指向别的东西. jquery就指向了原始元素.
不过如果我想在循环之中退出怎么办, 比如我找到了2, 就想退出循环.那么再修改一下.

Array.prototype.each = function(fn){
    for (var i = 0, l = this.length; i < l; i++){
        if ( fn.call(this[i], i, this[i]) === false ) break;  //当回调函数返回false的时候, 中止循环.
    }
};

[1, 2, 3].each(function(i, n){ if (n >= 2) { return false; } alert (n) })


现在已经基本达到目的了, 再来看看jquery里each的实现.
  each: function( callback, args ) {
        return jQuery.each( this, callback, args );
    },

直接交给静态方法jQuery.each来操作. 看看jQuery.each
jQuery.each
each: function( object, callback, args ) {
/
object为目标jquery对象, callback为回调函数, args表示callback方法里的参数, 如果不传, 就为默认的i(下标), n(当前元素).
/
   var name, i = 0,   //对象属性名、 数组下标
    length = object.length,
    isObj = length === undefined || jQuery.isFunction(object);
/
isObj判断object是单个对象还是数组, 如果是单个对象, 就遍历它的属性, 如是数组, 就循环每个元素.
/

if ( args ) { //如果给callback传递了参数 if ( isObj ) { //如果是单个对象 for ( name in object ) { //遍历属性 if ( callback.apply( object[ name ], args ) === false ) {
/ 执行回调函数, 如果回调函数返回false, 退出循环. 回调函数里的参数为自己传递的args. / break; } } } else { //如果是数组 for ( ; i < length; ) { //循环数组 if ( callback.apply( object[ i++ ], args ) === false ) { //执行回调函数, 如果回调函数返回false, 退出循环. break; } } }

    }  else {  //基本同上, 除了用i和value代替参数args
    if ( isObj ) {
            for ( name in object ) {
                if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
                    break;
                }
            }
        } else {
            for ( var value = object[0];
                i &lt; length &amp;&amp; callback.call( value, i, value ) !== false; value = object[++i] ) {}
        }
    }

    return object;
},


其实each方法还有一个小小的缺陷. 当要遍历一个对象的时候.
比如 { name: "John", lang: "JS" }这个对象. 这样写是没问题的.
   $.each( { name: "John", lang: "JS" }, function(i, n){
       alert( "Name: " + i + ", Value: " + n );
   })

但其实很多时候我们可能希望这样写
   $({ name: "John", lang: "JS" }).each(function(i, n){
        alert( "Name: " + i + ", Value: " + n );
    })
    

但这样写不行. 因为现在的目标对象object实际上是一个经过包装了的jquery对象. 即使里面是单个元素, 它的length也为1. isObj为false. jquery不会遍历它的属性.

这个方法里的代码其实也可以更精简一点,2个大的条件分支完全可以合并成一个.



jQuery.prototype.map

将一组元素转换成其他数组(不论是否是元素数组)

其实很容易联想到, map方法就是让集合里的每个元素都执行一次同一个函数, 把返回值填充到一个数组并返回这个新的数组.

看个例子
var ary = $.map( [0,1,2], function(n){
  return n + 4;
});

alert (ary);


ary已经被转化成为[4,5,6].

再看源码
    map: function( callback ) {
        return this.pushStack( jQuery.map(this, function( elem, i ) {
            return callback.call( elem, i, elem );
        }));
    },

看起来有一点点复杂, 其实callback.call( elem, i, elem )得到的是每个元素通过转化之后的值.在jQuery.map里,这些值都会被填充进一个新的数组.最后把原来的引用存入pushStack,方便回溯.


看看jquery.map
map: function( elems, callback, arg ) {
        var ret = [], value;
   //ret是待填充的数组.
   //value引用回调函数的返回值
        for ( var i = 0, length = elems.length; i < length; i++ ) {
            value = callback( elems[ i ], i, arg );
//执行回调函数. elems[ i ]表示当前元素值, i表示当前循环到了 //第几个元素. arg参数是1.26版本里没有的.只供内部使用 if ( value != null ) { //当返回值不为空时. ret[ ret.length ] = value; //push进新数组. } }

    return ret.concat.apply( [], ret );  

},


注意value != null, 这里只有2个等号, 意味着可以也过滤掉返回值为undefined的元素, 比如在map一堆dom节点的子节点时, 如果某个元素没有子节点, 那个元素就会被过滤掉.
可以看测试代码
<body>
        <div><span></span></div>
        <div><span></span></div>
        <div><span></span></div>
        <div><span></span></div>
        <div></div>
</body>
    <script>

var ary = $.map( document.getElementsByTagName(&#39;div&#39;), function(n){
  return n.childNodes[0];

}); 

alert (ary.length)
&lt;/script&gt;


jQuery.prototype.grep
使用一个过滤函数闭包来按照某种条件来过滤数组
grep: function( elems, callback, inv ) {
 //至少需要前2个参数, 待过滤的数组和过滤函数. 第3个参数没有什么必要.默认为false. 如果填true 就在过滤函数返回false的时候选择那个元素.
        var ret = [];  //空数组, 待会装载过滤后的元素.

    for ( var i = 0, length = elems.length; i &lt; length; i++ ) { 
 //循环待过滤数组.
        if ( !inv !== !callback( elems[ i ], i ) ) {  

//inv默认为undefined, callback也有可能返回undefined. 等号2端前面都加个!是为了让inv和//callback统一转化为boolean类型进行===的判断.其实可以简单看成 //inv !== callback( elems[ i ], i ) ) ret.push( elems[ i ] ); //符合条件的塞进数组 } } return ret; },


当过滤函数的返回结果不为undefined, "", 0, false, null这5种情况之一时, 通过!转化恰好不等于默认的inv(默认为false). 条件成立,这个就是需要的元素. 不过一般情况下我们让过滤函数返回true就可以了. 注意并没有用callback.call的形式来调用callback. 所以callback里的this是指向window的.



jQuery.prototype.ready

用来替代window.onload. 每个主流库中都有这个方法的相应实现.

如果使用window.onload, 你必须得等到页面的所有图片,视频等都加载完,才会触发window.onload里面的方法.

下面是页面加载的具体顺序.
onContentReady,这时DOM树完成
script defer 这时开始执行设定了defer属性的script.
             某些库比如ext用到了这个属性
ondocumentready complete这时可以使用HTC组件与XHR 
html.doScroll 这时可以让HTML元素使用doScroll方法
window.onload 这时图片flash等资源都加载完毕
来自http://www.cnblogs.com/rubylouvre/archive/2009/12/30/1635645.html


jQuery分别用到了onContentReady, html.doScroll, window.onload来确认dom最早加载完成的时间.
jQuery的实现可以大致分为这几个步骤.

1首先把需要在页面加载完成后执行的函数都存到一个list中, 加载完成后再依次触发.并设置一个控制器, 保证一个页面只有一个监听函数在执行.

2判断document.readyState是不是为complete.表示dom是否加载完毕

3 如果2步骤失败, 根据浏览器的不同,给document增加DOMContentLoaded或者onreadystatechange事件,当该事件被触发的时候执行readyList里的方法.

4在IE浏览器中, 设置一个定时器,不停的查看是否已经可以执行这个操作.document.documentElement.doScroll("left");如果执行这个操作时不再抛出异常,说明dom已经加载完毕了.这个办法可能比步骤3更快.

看看代码



ready: function( fn ) { jQuery.bindReady(); //添加监听函数

    if ( jQuery.isReady ) { //如果dom加载完成了
        fn.call( document, jQuery );  //立刻执行函数

    } else if ( readyList ) {
        readyList.push( fn );  //否则把函数添加进readyList
    }
    return this;
},


这里的逻辑并不复杂, 当调用$().ready(fn) 时, 先通过jQuery.isReady看dom有没有加载完成(jQuery.isReady默认是false, 当加载完成时会被设置为true).如果dom已经加载完成了,就立刻执行fn. 否则, 把fn添加进待执行的数组.等dom加载完成时再执行.


看看bindReady的实现
bindReady: function() {
    if ( readyBound ) {  //默认为false
        return;
    }
  readyBound = true;

if ( document.readyState === "complete" ) {
//如果$().ready()的时候,document已经加载完成了. return jQuery.ready(); //执行readyList里的方法 } if ( document.addEventListener ) {
  //如果支持w3c标准事件模型, 如firefox opera, safari document.addEventListener("DOMContentLoaded",DOMContentLoaded, false ); //当dom加载完成时, 触发DOMContentLoaded方法. DOMContentLoaded方 //法可查看源码742行.

window.addEventListener( &quot;load&quot;, jQuery.ready, false );  

  //保险起见, 给window.onload上面也绑定jQuery.ready, 这是其它方法都 //失效,逼不得以的情况.

} else if ( document.attachEvent ) {   //如果是IE事件模型

document.attachEvent("onreadystatechange", DOMContentLoaded); //实际上是判断当document.readyState === "complete"时, 执行 // readyList里的函数, 见源码754行.

    window.attachEvent( &quot;onload&quot;, jQuery.ready );  

  //同上面的window.addEventListener / 下面的代码只针对IE浏览器和页面不在iframe之中的情况, 当页面处在iframe中, 好像有时候用doScroll()会出问题 (不过我测试了几次, 似乎没有发现这个问题 - -!). / var toplevel = false;   try { toplevel = window.frameElement == null;
  //判断是IE并且页面不在iframe当中 } catch(e) {} if ( document.documentElement.doScroll && toplevel ) { doScrollCheck(); //不停的执行document.documentElement.doScroll("left"); 直到不报异常 } } }


再看看bindReady方法中涉及到的几个方法.
jQuery.ready
    ready: function() {
        if ( !jQuery.isReady ) {          
  if ( !document.body ) {
  //至少要保证document.body存在. 似乎又是为IE做的hack return setTimeout( jQuery.ready, 13 ); //每隔13ms调用, resig似乎对13这个数字情有独钟, 是因为比较接近cpu //平均一帧的时间? } jQuery.isReady = true; //设置isReady if ( readyList ) { var fn, i = 0; while ( (fn = readyList[ i++ ]) ) { fn.call( document, jQuery );
//依次执行readyList里的方法. } readyList = null; //清空readyList,尽早释放内存. 因为当isReady为true时, //$().ready().对于参数里方法,采取的是来一个执行一个.已经无须 // readyList了.

}

        if ( jQuery.fn.triggerHandler ) {  
            //触发document上绑定的事件
            jQuery( document ).triggerHandler( "ready" );
        }
    }
}


doScrollCheck
function doScrollCheck() {
    if ( jQuery.isReady ) {
        return;
    }

try {
    document.documentElement.doScroll("left");
} catch( error ) {
    setTimeout( doScrollCheck, 1 );   
    return;
}
//不停的执行document.documentElement.doScroll("left")
//直到没有异常抛出
jQuery.ready();

}


jQuery.proxy

proxy: function( fn, proxy, thisObject ) 返回一个新函数,这个函数的this指向你指定的对象.

jquery1.4总算提供了this代理的方法了, 以前总是要自己实现一个Function.prototype.bind方法.
可能很多同学对this的调用和指向还是有点模糊. 那在此之前,先讲一下this的几种指向情况.

1 普通函数调用, this指向window.
比如
var fn = function(){
alert (this === window)
};
fn();
结果为true

2 对象属性调用, this指向拥有这个属性的对象.
比如 var obj = {
fn: function(){
alert (this === obj);
}
obj.fn();
}
结果为true
或者
document.getElementById("id1").onclick = function(){
alert (this.id);
}
点击后弹出id1, 此时this指向onclick的拥有者id1.

3 通过构造函数调用this时,this指向通过构造函数生成的对象.
比如 function A(){
this.b = 1;
}
var a = new A();
当a去调用A的构造函数时, this是指的a.

4 call或者apply, 这里的this是由自己指定.
比如
(function(){
    alert(this.name);
}).call({name: "__游乐场"})

这里需要一个括号把function(){alert(this.name)}包围起来是为了让引擎把括号里面的语句当成一个表达式而不是函数声明, 函数声明是不能调用方法的. 编译期进行语法检测的时候就会报错.

其实我们在开发很容易就不知不觉弄丢了this.举个例子.我要点击一个div的时候,弹出这个div的id.
document.getElementById('div1').onclick = function(){
    ~function(){
        alert (this.id);
    }()
}

这个方法里的this就已经是window了. 一般我们可以改成这样
document.getElementById('div1').onclick = function(){
    var self = this;
    ~function(){
        alert (self.id);
    }()
}

也许你不喜欢self这个临时变量.那换一种方法.我们扩展一下Function的原型,实现一个最简单的bind方法.
Function.prototype.bind = function(obj){
    var self = this;
    return function(){ //返回一个闭包, 调用的时候把obj当成this.
        self.call(obj);
} }
document.getElementById('div1').onclick = function(){
    ~function(){
        alert (this.id);
    }.bind(this)()
}


现在已经OK了,没有了讨厌的临时变量. 这里的proxy方法肯定也是利用call或者apply方法来指定this. 看看api上的例子
var obj = {
    name: "John",
    test: function() {
      alert( this.name );
      $("#test").unbind("click", obj.test);
    }  };
    $("#test").click( jQuery.proxy( obj, "test" ) );  
// 以下代码跟上面那句是等价的:
// $("#test").click( jQuery.proxy( obj.test, obj ) ); // 可以与单独执行下面这句做个比较。 // $("#test").click( obj.test );

proxy根据参数传递的不同有2种调用方式,
1, 参数分别为obj对象, 被代理函数(必须是obj对象的属性). 结果是返回obj.test函数, this指向obj.
2, 参数分别为被代理函数(obj.test), this指向obj.

第二种方式看来顺眼得多.再看看源码的具体实现

proxy: function( fn, proxy, thisObject ) {
    //参数分别为被代理函数, this代理, thisObject是内部用的变量, 用来修正被代理函数
        if ( arguments.length === 2 ) {
            if ( typeof proxy === "string" ) {
//如果第二个参数为字符型, 参看上面的第一种调用方式. thisObject = fn;
//修正thisObject指向obj对象. 这里代码虽然写的fn, 看起来像一个函数 //实际上是第一种调用方式传递进来的某个对象. fn = thisObject[ proxy ];
//被代理函数修正为obj对象的某个属性函数. proxy = undefined;
//清空proxy, 跟下面的if ( !proxy && fn ) 统一处理只有一个参数fn的情况.

    } else if ( proxy &amp;&amp; !jQuery.isFunction( proxy ) ) {     

//上面第二种调用方式 thisObject = proxy;
//修正thisObject指向obj对象. proxy = undefined;
//清空proxy, 跟下面的 if ( !proxy && fn ) 统一处理只有一个参数fn的情况. } }

    if ( !proxy &amp;&amp; fn ) {                
 // 如果只有一个参数, proxy为undefined. 有2个参数的绝大部分情况下, proxy也已经被设置为undefined. 这里的条件判断都为true
        proxy = function() {             
            return fn.apply( thisObject || this, arguments );   

//进行代理, 如果只有一个参数, thisObject显然是undefined, 这里的代理对象还是原来的this. 注意proxy返回一个函数, 这里的第二个参数arguments是以后调用proxy函数时候的arguments }; }

    if ( fn ) {
        proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;  //设置一个全局标识符, 在event系统中用到.
    }

    return proxy;
}



浏览器特性检测

从jquery1.3版本开始, 写不同浏览器的兼容代码之前, 不赞成再去判断浏览器的类型, 而是直接判断支不支持某个特性, 比如盒模型. 透明度这些. 就像一个老外向你问路的时候, 他肯定是先说can you speak English. 而不是are you Amercan.

关于特性检测的具体实现, 可以参考下面文章.
http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
http://yura.thinkweb2.com/cft/
http://www.jibbering.com/faq/faq_notes/not_browser_detect.html

      <br><br>
      作者: <a href="http://gengzhelun619-163-com.javaeye.com">__游乐场</a> 
      <br>
      声明: 本文系JavaEye网站发布的原创文章,未经作者书面许可,严禁任何网站转载本文,否则必将追究法律责任!
      <br><br>
      <span style="color:red">
        <a href="http://www.javaeye.com/topic/818329" style="color:red">已有 <strong>1</strong> 人发表回复,猛击-&gt;&gt;<strong>这里</strong>&lt;&lt;-参与讨论</a>
      </span>
      <br><br><br>

JavaEye推荐