jQuery源码学习3——工具方法篇
基本工具方法结构如下:
jQuery.extend({ init:function(){}, each:function(){}, className:{ add:function(){}, remove:function(){}, has:function(){}, }, swap:function(){}, css:function(){}, curCSS:function(){}, clean:function(){}, expr:{}, token:[], find:function(){}, getAll:function(){}, attr:function(){}, parse:[], filter:function(){}, trim:function(){}, parents:function(){}, sibling:function(){}, merge:function(){}, grep:function(){}, map:function(){}, event:{ add:function(){}, guid:1, global:{}, remove:function(){}, trigger:function(){}, handle:function(){}, fix:function(){}, }, });
1、init本质上是在扩展实例方法,等分析完基本工具方法之后再分析这里
2、each map grep是一组基本的方法,这三个方法的功能也比较类似,map里面又调用了merge方法,因此待会儿先研究研究merge,再回过头来看each map grep如何实现
3、className相对独立,就是处理HTML元素的class类的,就是里面的正则稍微有些麻烦
4、swap方法在css和curCSS里面有用到,这些方法共同处理CSS样式
5、clean 有3个地方用到了clean 最开始的jQuery构造函数里面 domManip里面 wrap里面,clean方法的参数形如<div></div> <a></a> <table></table>
6、expr和token和parse分别是一个json,两个数组,好像是和选择器相关的,除此之外还有find和getAll和filter,这些东西都挺复杂的
7、trim方法用了一个很经典的正则/^\s+|\s+$/,后期版本在此基础上优化了,这个实在没什么好说的
8、parents和sibling是和DOM操作相关的方法
9、attr是属性操作的一个方法
10、event事件系统
第6部分选择器最麻烦,所以放到最后再看
为什么选择从基本工具方法开始研究呢?因为这些方法都是最底层的JavaScript,没有调用其他额外的顶层方法,相反的,要看一下这些方法在哪里调用,从而明确它的执行上下文是什么样的,参数是怎么样的等等
(1)、each
jQuery.each = function( obj, fn, args ) { if ( obj.length == undefined ) for ( var i in obj ) fn.apply( obj[i], args || [i, obj[i]] ); else for ( var i = 0; i < obj.length; i++ ) fn.apply( obj[i], args || [i, obj[i]] ); return obj; };
通常调用$.each方法的时候最后一个参数args默认是不传递的,例如
var arr=[4,5,6,7]; $.each(arr,function(i,v){ console.log(i,v); //i是数组的索引,v是数组的下标 });
由于传入了数组,所以obj有length属性,也就是说会走else分支
else里面通过普通的for循环对传进来的数组进行遍历
对数组中的每个元素分别执行我们传入的回调函数
由此也可以看出第一个实参是for循环中每次都递增的i
第二个实参是下标为i对应的数组项的值
传入json对象的时候也是同样的道理
另外,在调用fn的时候还通过apply改变执行环境,也就是将this的指向强行修改为每次循环到的项
值得注意的是不论传入的是数组,还是json
如果里面每一项的值是基本类型的话,回调里面不论怎么改原数组或对象都不会变
而如果里面的每一项的值是对象类型的话,回调里面为this增加或删除成员会影响到原来的数组或json
(2)、grep
grep: function(elems, fn, inv) { if ( fn.constructor == String ) fn = new Function("a","i","return " + fn); var result = [];
for ( var i = 0; i < elems.length; i++ ) if ( !inv && fn(elems[i],i) || inv && !fn(elems[i],i) ) result.push( elems[i] ); return result; }
如果要是不接触jQuery源码的话,平时工作基本上没有机会遇到new Function()包装对象这种东西
刚刚查了一下资料才知道它的用法:new Function("param1","param2"...,"body")
即最后一个是函数体,前面的都是参数,而且所有的这些都是字符串类型
grep通常用法如下:
var arr=[1,2,3,4,5,6,7,8]; var filterArr=$.grep(arr,function(v,i){//按照function里面的条件筛选arr数组 if(v%2){ return true;//如果符合条件就return true } }); console.log(filterArr);//[1,3,5,7]
其实我们平时做开发的时候就和上面这种用法差不错
第二个参数以字符串形式传入然后通过包装对象初始化fn是jQuery内部为了简化操作而做的
在后面的很多方法中用到了这个特性
(3)、merge
merge: function(first, second) { var result = []; //先把first里面所有项全部放到result当中 for ( var k = 0; k < first.length; k++ ) result[k] = first[k]; //在将second里面的项往result里面放的时候,先判断一下first里面是否已经有这个项了,如果没有的话再放 for ( var i = 0; i < second.length; i++ ) { var noCollision = true; for ( var j = 0; j < first.length; j++ ) if ( second[i] == first[j] ) noCollision = false; if ( noCollision ) result.push( second[i] ); } return result; },
(4)、map
map: function(elems, fn) { if ( fn.constructor == String ) fn = new Function("a","return " + fn); var result = []; for ( var i = 0; i < elems.length; i++ ) { var val = fn(elems[i],i); if ( val !== null && val != undefined ) { if ( val.constructor != Array ) val = [val]; result = jQuery.merge( result, val ); } } return result; },
开头通过Function包装对象处理传入的fn为字符串的情况
和grep一样也是jQuery内部调用的形式
不管是哪种传入形式,经过这个fn处理之后
判断一下返回值是不是一个有意义的值(如果是null或者undefined就会剔除掉)
再判断返回值是否是数组,如果是数组那么直接merge
不是数组的话,新建一个数组对象val
里面只存放一个元素,就是刚刚处理得到的结果
然后都merge到result数组中
最终将result数组返回
(5)、className
className: { add: function(o,c){ if (jQuery.className.has(o,c)) return; o.className += ( o.className ? " " : "" ) + c; }, remove: function(o,c){ o.className = !c ? "" : o.className.replace( new RegExp("(^|\\s*\\b[^-])"+c+"($|\\b(?=[^-]))", "g"), ""); }, has: function(e,a) { if ( e.className != undefined ) e = e.className; return new RegExp("(^|\\s)" + a + "(\\s|$)").test(e); } },
先看has,有两个参数,意思其实很明显
就是判断元素e是否有a这个类
刚开始的时候我理解成了通过e.className!=undefined来判断元素e上是否加了class这个属性
但是经过测试发现不论是写成
<div></div>
还是写成
<div class="abc"></div>
e.className!=undefined都返回true
因为即使没有加class,只要e是DOM对象,e.className就会返回字符串
只不过不加class返回空字符串,而加了class就返回对应的类而已
因此我猜测这个地方应该是剔除掉传进来的e是js对象的情况
例如:$.className.has({"a":1},"abc");
这种方式调用虽然在new RegExp("(^|\\s)" + a + "(\\s|$)").test(e)这个正则中不符合方法test中参数的类型
不过经过测试,也没有报错,估计是调用了e.toString()方法了吧
通过分析new RegExp("(^|\\s)" + a + "(\\s|$)")这个正则得知
该正则匹配的就是类a在开头或者结尾或者a的两边有空格的情况
这就是has的大概逻辑
接下来再看add就很简单了,先通过has方法判断元素o有没有c这个类
如果有的话就不再重复添加,直接返回
没有的话再看o这个元素有没有添加class这个属性
如果没有的话直接将c这个类赋值给class,如果有的话就在class属性值后面追加c
最后看remove
由于我从来没有专门记过运算符的优先级
以至于每次看到别人写的代码看不懂的时候都是现场搜索
这次也不例外,初看remove的代码的时候
还以为是先执行o.className = !c
然后将o.className = !c的值作为判断条件去执行后面的三目
后来网上搜了一下才知道赋值运算符的优先级是最低的
因此,这段代码是先判断传进来的类c的值
比较正常的传法就是$.className.remove(oDiv,"abc");
即删除oDiv上的abc类
那这时肯定就走到了
o.className.replace(new RegExp("(^|\\s*\\b[^-])"+c+"($|\\b(?=[^-]))", "g"), "");
接下来分析一下这个正则:
其实正则里面的匹配项并不一定非得匹配某个特定的字符或某一串字符串
也可以是匹配一个位置,比如说这个正则中的\b就是这种情况
这个正则主要限制类的前后两端不能是"-" 或者以这个类开头 或者以这个类结尾