Jquery源码分析与简单模拟实现
前言
最近学习了一下jQuery源码,顺便总结一下,版本:v2.0.3
主要是通过简单模拟实现jQuery的封装/调用、选择器、类级别扩展等。加深对js/Jquery的理解。
正文
先来说问题:
1.jQuery为什么能使用$的方式调用,$是什么、$()又是什么、链式调用如何实现的
2.jQuery的类级别的扩展内部是怎样实现的,方法级别的扩展有是怎样实现的,$.fn又是什么
3.jQuery选择器是如何执行的,又是如何将结果包装并返回的
带着这些问题,我们进行jquery的模拟实现,文章下方有demo代码。
a.关于$
1 //@spring:window:便于压缩,查找速度要快 undefined:ie7ie8是可以被修改如var undefined = 10;,为了防止外界改变 2 (function (window, undefined) { 3 var jQuery = { 4 }; 5 6 if (typeof window === "object" && typeof window.document === "object") { 7 window.jQuery = window.$ = jQuery; 8 } 9 }(window));
jquery用了个自执行方法封装了一下,传入window对象是为了便于压缩,相当于给了个临时变量,像jquery声明的以下变量也是这个作用
1 var 2 // A central reference to the root jQuery(document) 3 rootjQuery,//@spring:html文件的document节点 4 5 // The deferred used on DOM ready 6 readyList,//@spring:dom加载相关 7 8 // Support: IE9 9 // For `typeof xmlNode.method` instead of `xmlNode.method !== undefined` 10 core_strundefined = typeof undefined,//@spring:xmlnode判断的时候会产生bug,所以用typeof来判断 11 12 // Use the correct document accordingly with window argument (sandbox) 13 location = window.location,//@spring:这些存储都是为了便于压缩操作,如location=window.location;location会压缩成i,l等 14 document = window.document,//@spring:同上 15 docElem = document.documentElement,//@spring:同上 16 17 // Map over jQuery in case of overwrite 18 _jQuery = window.jQuery, 19 20 // Map over the $ in case of overwrite 21 _$ = window.$,//冲突解决 22 23 // [[Class]] -> type pairs 24 class2type = {},//类似两个字符串组成的[{'[Object String]','[spring]'}] 25 26 // List of deleted data cache ids, so we can reuse them 27 core_deletedIds = [], 28 29 core_version = "2.0.3", 30 31 // Save a reference to some core methods 32 core_concat = core_deletedIds.concat, 33 core_push = core_deletedIds.push, 34 core_slice = core_deletedIds.slice, 35 core_indexOf = core_deletedIds.indexOf, 36 core_toString = class2type.toString, 37 core_hasOwn = class2type.hasOwnProperty, 38 core_trim = core_version.trim,
b.再看$()或者$("***"),也就是jquery的构造函数。先看jq源码
1 // Define a local copy of jQuery 2 jQuery = function (selector, context) { 3 // The jQuery object is actually just the init constructor 'enhanced' 4 return new jQuery.fn.init( selector, context, rootjQuery ); 5 },
selector:是个对象,最常见的就是字符串选择器,其他还有好多类型,下面会不断给出说明。
context:数据上下文,也就是个范围限定,平时用的少些。比如$(".highlight","#div1")就是找id为div1下面的所有class为highlight。不传就是document
new jQuery.fn.init( selector, context, rootjQuery ):使用jQuery.fn.init初始化构造jquery对象,jQuery.fn是啥,看源码截图:
jQuery.fn就是jQuery.prototype,所以想想对象级别的扩展就是prototype下扩展方法而已。那么init也就是jquery下面的一个扩展方法了
讲到这里我们先模拟一下过程
1 (function (window, undefined) { 2 var jQuery = function (selector) { 3 return new jQuery.fn.init(selector); 4 }; 5 jQuery.fn = jQuery.prototype = { 6 jquery: "spring-1.0.js",//jquery版本 7 init: function (selector) {//context,rootjQuery不传了,稍微看看就懂了 8 console.log("对" + selector + "进行处理"); 9 } 10 } 11 12 if (typeof window === "object" && typeof window.document === "object") { 13 window.jQuery = window.$ = jQuery; 14 } 15 }(window)); 16 $("input[name='age']");
看下jq内部的init实现过程(已将详细实现代码剔除,只看结构)
看看Jquery选择器返回的数据结构。
啥都查不到时,jQuery.fn.jQuery.init[0],看起来像个数组。有个length就是查询到的数据长度。有个context 指向document,context 也就是上面所述的上下文(查找范围)
查找到数据时,更像个数组了。0/1是查到的元素,length是长度。在chrome输出台输出的也是个数组。挺奇怪的!
这些都很奇怪,而且更奇怪的是new jQuery.fn.init(selector),实例化的是init对象,init里面没有这些ajax/add/append/css等方法或属性,这些都是jquery的属性/方法。
_proto_是指向init的prototype的(关于_proto_是啥,每个对象初始化实例都会生成一个_proto_指向该对象的prototype。简单说下,其他的自行百度研究一下),却为啥会指向jQuery.prototype。
查一下jQuery源码,没啥玄虚,手动改指向。这样new了init对象,执行也查询方法,同时又指向了Jquery,这才有了$().各类方法。如下:
前面一直说查询的元素像个数组,像个数组但不是数组,它是一个对象。怎么做的呢,我们把init方法模拟下一起说
1 //辅助:jquery合并数组的方法 2 function merge(first, second) { 3 var l = second.length, 4 i = first.length, 5 j = 0; 6 7 if (typeof l === "number") { 8 for (; j < l; j++) { 9 first[i++] = second[j]; 10 } 11 } else { 12 while (second[j] !== undefined) { 13 first[i++] = second[j++]; 14 } 15 } 16 17 first.length = i; 18 19 return first; 20 } 21 22 23 (function (window, undefined) { 24 var core_version = "spring v.1", 25 core_deletedIds = [], 26 core_push = core_deletedIds.push, 27 core_slice = core_deletedIds.slice; 28 var jQuery = function (selector) { 29 return new jQuery.fn.init(selector); 30 }; 31 jQuery.fn = jQuery.prototype = { 32 jquery: core_version,//jquery版本 33 constructor: jQuery,//覆盖构造函数防止被外部改变 34 init: function (selector) {//context,rootjQuery不传了,稍微看看就懂了 35 //针对不同参数类型进行不同处理方式,如果$("")$(null就直接返回) 36 if (!selector) { 37 //参数不对直接将this返回,想想现在this的值是什么,提示:new init();=>jQuery.fn.init[0] 38 return this; 39 } else { 40 //如果是字符串juqery会调用查询方法进行查询dom元素(jquery调用sizzle专门进行dom解析) 41 var nodes = document.getElementsByName("age"); 42 var arr = []; 43 for (var i = 0; i < nodes.length; i++) { 44 arr.push(nodes[i]); 45 } 46 //如果传递了Context上下文,则在context中寻找元素。这里指定位document 47 this.context = document; 48 //把selector存到jQuery中 49 this.selector = selector; 50 //jquery的合并方法,直接拿出来就能用,合并查询结果 51 var result = merge(this, arr); 52 //对处理过的this进行封装返回,注意为了链式调用,都需要返回this 53 return result; 54 } 55 }, 56 selector: "" 57 } 58 jQuery.fn.init.prototype = jQuery.fn; 59 if (typeof window === "object" && typeof window.document === "object") { 60 window.jQuery = window.$ = jQuery; 61 } 62 }(window)); 63 $(".test");
其实代码里没啥东西都是模仿jquery的。不过就是简化一下,模仿一下。所以要先看结构这样才知道简化哪句,模仿那句。
看下结果:
结果查出来了,但是不像数组啊,四不像的。init后面也没有个[]啊。
看下jQuery源码:
关键代码就这这里,让对象像个数据加这几句就行了,我们来试试(完整的代码):
1 <input type="text" class="test" name="age" /> 2 <input type="text" class="test" name="Name" /> 3 <div class="test"></div> 4 <script> 5 //辅助:jquery合并数组的方法 6 function merge(first, second) { 7 var l = second.length, 8 i = first.length, 9 j = 0; 10 11 if (typeof l === "number") { 12 for (; j < l; j++) { 13 first[i++] = second[j]; 14 } 15 } else { 16 while (second[j] !== undefined) { 17 first[i++] = second[j++]; 18 } 19 } 20 21 first.length = i; 22 23 return first; 24 } 25 26 27 (function (window, undefined) { 28 var core_version = "spring v.1", 29 core_deletedIds = [], 30 core_push = core_deletedIds.push, 31 core_slice = core_deletedIds.slice; 32 var jQuery = function (selector) { 33 return new jQuery.fn.init(selector); 34 }; 35 jQuery.fn = jQuery.prototype = { 36 jquery: core_version,//jquery版本 37 constructor: jQuery,//覆盖构造函数防止被外部改变 38 init: function (selector) {//context,rootjQuery不传了,稍微看看就懂了 39 //针对不同参数类型进行不同处理方式,如果$("")$(null就直接返回) 40 if (!selector) { 41 //参数不对直接将this返回,想想现在this的值是什么,提示:new init();=>jQuery.fn.init[0] 42 return this; 43 } else { 44 //如果是字符串juqery会调用查询方法进行查询dom元素(jquery调用sizzle专门进行dom解析) 45 var nodes = document.getElementsByName(selector); 46 var arr = []; 47 for (var i = 0; i < nodes.length; i++) { 48 arr.push(nodes[i]); 49 } 50 //如果传递了Context上下文,则在context中寻找元素。这里指定位document 51 this.context = document; 52 this[0] = document; 53 //把selector存到jQuery中 54 this.selector = selector; 55 //jquery的合并方法,直接拿出来就能用,合并查询结果 56 var result = merge(this, arr); 57 //对处理过的this进行封装返回,注意为了链式调用,都需要返回this 58 return result; 59 } 60 }, 61 selector: "", 62 length: 0, 63 toArray: function () { 64 return core_slice.call(this); 65 }, 66 get: function (num) { 67 return num == null ? 68 this.toArray() : 69 (num < 0 ? this[this.length + num] : this[num]); 70 }, 71 //这里要注意,想要长得像jquery.fn.jquery.init[0],并且init方法中的this值为数组就必须加下面这三个字段 72 push: core_push, 73 sort: [].sort, 74 splice: [].splice 75 } 76 jQuery.fn.init.prototype = jQuery.fn; 77 if (typeof window === "object" && typeof window.document === "object") { 78 window.jQuery = window.$ = jQuery; 79 } 80 }(window)); 81 $("age"); 82 83 </script>
看看输出结果:
恩恩,不错不错…挺像的。
这就是对选择器的简单模拟。其实jQuery也是调用Sizzle.js进行html元素解析的(牵涉许多,不多讲了,自己去查吧)
至于jQuery对象级别的扩展,简单模拟一个,其实就是jQuery.prototype.method扩展一个方法而已
//jquery对象级别的扩展插件,看看就明白是啥了 jQuery.fn.css = function (className) { //注意this是一个对象,length值是手动赋予的 for (var i = 0; i < this.length; i++) { var item = this[i];//通过下标找元素,this不是数组 item.setAttribute("class", className); } return this;//链式调用返回this };
调用如:
我们自己扩展一个:
1 //对象级别的扩展插件 2 $.fn.attr = function (name, value) { 3 for (var i = 0; i < this.length; i++) { 4 var item = this[i]; 5 if (name && value) { 6 item.setAttribute(name, value); 7 } else if (name && !value) { 8 return item.getAttribute(name); 9 } 10 } 11 return this; 12 };
调用一下,结果没错。返回this,也是为了链式调用。
如上所示比较简单,不多说。
然后就是所谓的类级别的扩展了,也就是jquery的静态方法。经常被写为$.method(如$.ajax)。实现的时候呢用的是$.extend({方法对象,写各种扩展方法})
$.extend是啥,看看源码:
其实就是jQuery的一个扩展方法,接收argument参数,这个参数就是你传过来的方法对象了,使用argument[0]一个个获取就行了
获取完了就是怎么把这些方法合并到jQuery本身了。看了下jquery源码,也来模拟下extend吧。
先看个小demo:
一看就懂,Person本身就是个对象,给它加个方法而已(Person又是啥对象呢,越讲越多,讲不完滴)。你把Person看成jQuery,那就是$.ajax。
再看下面这个模拟jQuery的方法:
1 //jquery静态方法扩展,即类级别扩展 2 jQuery.extend = jQuery.fn.extend = function () { 3 var src, copy, options, target = this; 4 ////arguments[0] 如{a:function(){},b:funciton(){}},一个参数对象 5 if ((options = arguments[0]) != null) { 6 for (var name in options) { 7 copy = options[name]; 8 target[name] = copy;//其实jquery就是把这些参数取出来,然后一个个复制到jquery这个object中 9 //如 var Person=function(){};Person.ajax=function(){}一样 10 } 11 } 12 };
关键代码第一句:target=this;this是啥或者说jQuery.fn.extend中的this是啥,其实就是jQuery对象。
关键代码第二句:for (var name in options),option就是你传递的那个对象,循环那个对象如:
var options={
ajax: function () {
console.log("模拟执行ajax");
},
load: function () {
console.log("模拟执行load");
}
}
关键代码第三句:target[name] = copy,其实也就是:
jQuery["ajax"]=function(){
console.log("模拟执行ajax");
}
结合前面Person的demo一下子就明白了。
然后我们就可以写出下面的jQuery方法了
1 /*****调用演示******/ 2 //函数级别的扩展插件 3 $.extend({ 4 ajax: function () { 5 console.log("模拟执行ajax"); 6 }, 7 load: function () { 8 console.log("模拟执行load"); 9 } 10 }); 11 $.ajax();
以上就是全部正文,本文全部代码:http://git.oschina.net/GspringG/jQueryDemo
总结
其实这篇也是越讲越多,js/jQuery的点是非常多的,也是越说越有意思。当然了本文也有可能出现一些有误的地方,请大家及时告知。