【JS】jQuery部分源码剖析

168 阅读5分钟

小总结

看源码的时候要注意:看清楚变量代表的是什么,变量代表的是它里面存储的值,而不是变量本身,这一点不要混淆,别看错了,尤其里面会用一些关键词作为变量名,弄清楚变量里面存的是什么,它代表的就是谁。看到了不知道的变量就往前或者往后在附近找一找,或者直接用查找功能搜一搜,找到它里面的源码到底存的是什么。

jQuery里面用到了很多 js 的思想和原生的方法属性,比如闭包的思想,面向对象的思想,数组的方法,还有一些数据类型转化的小技巧,数据类型参与判断的时候的小技巧等等,还有封装一个方法的时候要考虑的点有很多,一定要严谨,可以在源码里面学到很多自己封装一个方法的时候需要注意的事情,需要考虑的方面有哪些。

下面是部分源码解读

判断执行环境

1、区分浏览器环境 和 node 环境

2、如何判断是否支持CommonJS规范

3、jQuery放在闭包里面,可以利用 return 或者 window. 的方式把里面的方法暴露给外面,来供外面调用里面的这些方法


  <script>
    (function (global, factory) {
      // global--> window  factory--> 函数
      if (typeof module === "object" && typeof module.exports === "object") {
        // 支持Common.js规范,(比如在node.js里运行)
        // module.exports = global.document ?
        //   factory(global, true) :
        //   function (w) {
        //     if (!w.document) {
        //       throw new Error("jQuery requires a window with a document");
        //     }
        //     return factory(w);
        //   };

      } else {
        factory(global); // 实参变量global的值为 window
      }
    }(typeof window !== "undefined" ? window : this,function(window, noGlobal){
      // window --> window    noGlobal -->undefined
    }))
  </script>

普通函数执行就是创建实例

4、创造的是init这个类的一个实例,代码中让init.prototype=jQuery.prototype:所以最后创造的实例基于____proto____ 查找的时候,找的也是jQuery.prototype原型上的方法,所以我们也可以认为创造的是jQuery这个类的一个实例

目的是: 控制jQuery执行的时候当做普通的函数执行,但是能创造属于自己的一个实例

JQ的选择器: $()其实就是创建了JQ类的一个实例(“JQ对象”)

注意:

  1. 源码中的jQuery.fn 就是代表的jQuery的原型了

  2. jQuery和,他们的属性值都是当前的jQuery函数,所以在使用时用,他们的属性值都是当前的jQuery函数,所以在使用时用 () 就是jQuery执行,而且相当于构造函数执行,创建了一个jQuery实例对象

  3. this是原生js的对象,在jQuery中要使用 $(this) 才是jQuery对象

 jQuery充分体现了函数的三种角色:普通函数、构造函数、普通对象
 1. 写在原型上的方法是供实例“JQ对象”调用的  $('.box').xxx()
 2. 写在jQuery对象上的私有属性方法  $.xxx()  =>一般提供一些工具类方法,供项目开发或者JQ内部调用的

下面是源码:

      var jQuery = function (selector, context) { 
        // 这块其实是new 了一个init的实例返回出去,这样写的目的是为了避免死递归
        return new jQuery.fn.init(selector, context);
      };

      jQuery.fn = jQuery.prototype = {
        // 给jQuery的原型进行重定向,而且还给jQuery新增一个私有属性,其属性值也是当前这个新的原型
        jquery: version,

        constructor: jQuery, // 重定向之后的原型没有constructor,所以人家给手动加一个
        
       //此处省略了一些代码,后面详细讲这里的一些方法,比如get、each方法,还有一些原生js的方法
      }
      var init = jQuery.fn.init = function( selector, context ) {
        // 次处的类才是$()运行的真正的类
      }
      init.prototype = jQuery.fn;
      // 把init类的原型指向了jQuery的原型

      if (typeof noGlobal === "undefined") {
        window.jQuery = window.$ = jQuery;
        // 给window增加两个键值对,属性名是jQuery和$,他们的属性值都是当前的jQuery函数
      }
    }))

可以自己做一些尝试:

    // $('#tabList li'); // 他就是jQ的实例
    // jQuery(('#tabList li')
    // $()但返回值其实是init的实例

   // $(this)
   // $('<div></div>')

    // $('#tabList li').get()

    var jQuery = function(){
      return new jQuery();
    }

$():jQuery选择器、入口函数、对象转化

5、总结:jQuery执行时(创建实例)传的参数

  1. 参数传选择器的情况

  2. 参数传元素节点的情况

  3. 参数传函数的情况 --> 入口函数

  4. jQuery元素对象和原生js的元素对象之间可以相互转化,把jQuery对象转化为原生js对象只需要调用get方法或者后面加一个 中括号0 即可

基于$执行所创建出来的东西都是jq的实例,所以可以去调用jq原型上的方法
      1、不管传参还是不传参,返回值都是一个对象类型的实例
        $() ==>如果jq不穿参,那返回结果是一个空实例
      2、$执行时可以传三种不同的格式
        1)
          $('.box') jq的选择器
          $('<div>123</div>')
        2)
          $(原生的元素):把原生的元素变成jq的实例(只有这样草可以去调用jq原型上的方法)
          原生js中的方法jq中的方法不能混着去使用
          
        3) $(函数)  入口函数
           $(function(){}); <==> $(document).ready(function(){})
           //这俩是等价的

get方法的源码:

  // 把jq的实例转成原生的数组集合
        get: function (num) {
          return num != null ?
          
            // Return just the one element from the set
            (num < 0 ? this[num + this.length] : this[num]) :

            // Return all the elements in a clean array
            slice.call(this);
        }

注意: jQuery中的入口函数和原生js中的 window.onload 的区别

 window.onload = function(){}
// 等到页面的dom,内容,富媒体全部加载完成之后才会执行
 
 $(function(){}); ==>// $(document).ready(function(){})
// 而ready等到页面的dom加载完就执行

    rootjQuery.ready( selector )
    console.log( $(function(){}));

下面是$()的源码,有多种情况:

var rootjQuery,

	// Use the correct document accordingly with window argument (sandbox)
	document = window.document,

	// A simple way to check for HTML strings
	// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
	// Strict HTML recognition (#11290: must start with <)
	rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,

	init = jQuery.fn.init = function( selector, context ) {

		var match, elem;

		// HANDLE: $(""), $(null), $(undefined), $(false)
		if ( !selector ) {
			return this;
		}

		// Handle HTML strings
		if ( typeof selector === "string" ) {
			// $('<div/>123</div>')
			if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
				// Assume that strings that start and end with <> are HTML and skip the regex check
				match = [ null, selector, null ];
				console.log(match);

			} else {
				match = rquickExpr.exec( selector );
			}

			// Match html or make sure no context is specified for #id
			if ( match && (match[1] || !context) ) {
				// HANDLE: $(html) -> $(array)
				if ( match[1] ) {
					context = context instanceof jQuery ? context[0] : context;
                   console.log(context);
					// scripts is true for back-compat
					// Intentionally let the error be thrown if parseHTML is not present
					jQuery.merge( this, jQuery.parseHTML(
						match[1],
						context && context.nodeType ? context.ownerDocument || context : document,
						true
					) );

					// HANDLE: $(html, props)
					if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
						for ( match in context ) {
							// Properties of context are called as methods if possible
							if ( jQuery.isFunction( this[ match ] ) ) {
								this[ match ]( context[ match ] );

							// ...and otherwise set as attributes
							} else {
								this.attr( match, context[ match ] );
							}
						}
					}

					return this;

				// HANDLE: $(#id)
				} else {
					elem = document.getElementById( match[2] );

					// Check parentNode to catch when Blackberry 4.6 returns
					// nodes that are no longer in the document #6963
					if ( elem && elem.parentNode ) {
						// Handle the case where IE and Opera return items
						// by name instead of ID
						if ( elem.id !== match[2] ) {
							return rootjQuery.find( selector );
						}

						// Otherwise, we inject the element directly into the jQuery object
						this.length = 1;
						this[0] = elem;
					}

					this.context = document;
					this.selector = selector;
					return this;
				}

			// HANDLE: $(expr, $(...))
			} else if ( !context || context.jquery ) {
				return ( context || rootjQuery ).find( selector );

			// HANDLE: $(expr, context)
			// (which is just equivalent to: $(context).find(expr)
			} else {
				return this.constructor( context ).find( selector );
			}

		// HANDLE: $(DOMElement)
		} else if ( selector.nodeType ) {

			this.context = this[0] = selector;
			this.length = 1;
			return this;

		// HANDLE: $(function)
		// Shortcut for document ready
		} else if ( jQuery.isFunction( selector ) ) {
			return typeof rootjQuery.ready !== "undefined" ?
				rootjQuery.ready( selector ) :
				// Execute immediately if ready is not present
				selector( jQuery );
		}

		if ( selector.selector !== undefined ) {
			this.selector = selector.selector;
			this.context = selector.context;
		}

		return jQuery.makeArray( selector, this );
	};

// Give the init function the jQuery prototype for later instantiation
init.prototype = jQuery.fn;

// Initialize central reference
rootjQuery = jQuery( document );

noConflict避免冲突

6、noConflict方法:用来转让jQuery这两个变量的使用权的,把 和 jQuery 这两个变量的使用权的,把 和 jQuery 这两个变量的使用权转让回原来他们代表的东西,然后自己定义一个变量来接收noConflict这个方法执行的返回结果,也就可以让这个变量来代表jQuery的方法了。

比如已经引了一些插件或者类库进来,而这些插件或者类库里面用了jQuery作为变量名,防止把jQuery引进来以后,覆盖了前面已经引进来的类库里面的 或jQuery作为变量名,防止把jQuery引进来以后,覆盖了前面已经引进来的类库里面的或jQuery。

提供一个noConflict方法,防止污染覆盖window中原始的jQuery属性,调用noConflict方法,将原始的 和 jQuery 属性,调用noConflict方法,将原始的 和 jQuery 还原回到原来代表的东西,比如zepto,可以自己定义一个变量,来代表jQuery,然后使用这个变量来调用jQuery的方法也可以

noConflict源码:

  var
        // Map over jQuery in case of overwrite
        _jQuery = window.jQuery,

        // Map over the $ in case of overwrite
        _$ = window.$;

      jQuery.noConflict = function (deep) {
        if (window.$ === jQuery) {
          window.$ = _$;
        }

        if (deep && window.jQuery === jQuery) {
          window.jQuery = _jQuery;
        }

        return jQuery;
      };

// 如果一个页面引用的两个类库,但是都给window赋值一个$属性,人家jq为了避免这样的情况出生,在jq的身上有一个noConflict方法,是专门用来改变$和jQuery的使用权的
    // console.log($); // 这是JQuery函数

    let res = $.noConflict(true);
    // 如果函数执行时不穿参,那仅仅是把$ 的使用权给别人,但是如果传递一个true,那就是把jQuery的使用权也给别人,
    // 如果把使用权都给别人的话,那咱们只能使用当前函数的返回值了

    console.log($); // jq把$的使用权给别人了 

    console.log(res);
    console.log(jQuery);

可以自己测试一下前面提过的方法:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div>1</div>
  <div>2</div>
  <div>3</div>
  <div>4</div>
  <div id="box">5</div>
  <script src="./jquery-1.11.3.js"></script>
  <script>
    // console.log($('div').get());
    // console.log($.each()); 
    // var ss = $.noConflict();
    // window.$ = 'xxx';

    // console.log($());
    // console.log($('<div></div>'));
    // console.log($(box));


  </script>
</body>
</html>

7、类数组转数组的方法:toArray

 toArray: function () { // 类数组转数组
          return slice.call(this);
        }

原生方法不多做描述

        push: push,
        sort: deletedIds.sort,
        splice: deletedIds.splice
        // .....
        // 这三个方法都是js原生的方法

功能强大的each方法

8、功能强大的each方法:循环遍历,在jQuery本身和在原型上都有each方法

//在jQuery本身 和 在原型上 都有each方法,但是作用不一样
//在原型上的是供jQuery实例调用的
//在本身的是 直接调用 可以遍历数组或者对象
// ==》 遍历dom  (遍历jQuery对象)  
 $("ul>li").each(function(i,item){
    console.log(i);
   console.log(item);
 })
var ary=[4,5,6];
var obj={"name":"lili","age":18}

//===>遍历数据、数组
 $.each(ary,function(i,item){
     console.log(i);
    console.log(item);
 })

//===>遍历对象  for in
$.each(obj,function(i,item){
    console.log(i);   // 属性名
   console.log(item); // 属性值
})

与原生js中的each遍历有很多不同

//jQuery中的each方法还可以自己设置中断遍历的位置
//在源码中可以看到 value === false的时候,遍历就中止了
//所以我们在使用each方法的时候,在回调函数中,可以设置一个判断,
//符合某些条件的时候 return false  中止遍历

下面是each的源码:

//jQuery原型上的each源码
each: function (callback, args) {
        return jQuery.each(this, callback, args);
        }
//jQuery本身身上的each方法源码

each: function( obj, callback, args ) {
		var value,
			i = 0,
			length = obj.length,
			isArray = isArraylike( obj );

		if ( args ) {
			if ( isArray ) {
				for ( ; i < length; i++ ) {
					value = callback.apply( obj[ i ], args );

					if ( value === false ) {
						break;
					}
				}
			} else {
				for ( i in obj ) {
					value = callback.apply( obj[ i ], args );

					if ( value === false ) {
						break;
					}
				}
			}

		// A special, fast, case for the most common use of each
		} else {
			if ( isArray ) {
				for ( ; i < length; i++ ) {
					value = callback.call( obj[ i ], i, obj[ i ] );

					if ( value === false ) {
						break;
					}
				}
			} else {
				for ( i in obj ) {
					value = callback.call( obj[ i ], i, obj[ i ] );

					if ( value === false ) {
						break;
					}
				}
			}
		}

		return obj;
	}

给jQuery扩展方法

9、extend方法,用于扩展方法

 $.extend({
// 注意如果你传的属性名和jq的重名了,一般情况你是可以覆盖人家jq的,但是你不想把人家jq的方法进行覆盖,那就在extend执行的时候收个参数传一个false
        queryUrlParams:function(){

        },
        ajax:function(){
          console.log(23);
        }
        
      });
      console.dir($.ajax())

注意: jQuery.extend 和 jQuery.fn.extend 不一样 一个是给jQuery本身扩展方法 一个是给原型扩展方法

语法:
$.extend( target [, object1 ] [, objectN ] )

指示是否深度合并
$.extend( [deep ], target, object1 [, objectN ] )

下面是extend源码:

jQuery.extend = jQuery.fn.extend = function() { // true {}
	var src, copyIsArray, copy, name, options, clone,
		target = arguments[0] || {},
		i = 1,
		length = arguments.length,
		deep = false;

	// Handle a deep copy situation
	if ( typeof target === "boolean" ) {
		deep = target;

		// skip the boolean and the target
		target = arguments[ i ] || {};
		i++;
	}

	// Handle case when target is a string or something (possible in deep copy)
	if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
		target = {};
	}

	// extend jQuery itself if only one argument is passed
	if ( i === length ) {
		target = this;
		i--;
	}

	for ( ; i < length; i++ ) {
		// Only deal with non-null/undefined values
		if ( (options = arguments[ i ]) != null ) {
			// Extend the base object
			for ( name in options ) {
				src = target[ name ];
				copy = options[ name ];

				// Prevent never-ending loop
				if ( target === copy ) {
                    continue;
                }

				// Recurse if we're merging plain objects or arrays
				if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
					if ( copyIsArray ) {
						copyIsArray = false;
						clone = src && jQuery.isArray(src) ? src : [];

					} else {
						clone = src && jQuery.isPlainObject(src) ? src : {};
					}

					// Never move original objects, clone them
					target[ name ] = jQuery.extend( deep, clone, copy );

				// Don't bring in undefined values
				} else if ( copy !== undefined ) {
					target[ name ] = copy;
				}
			}
		}
	}

	// Return the modified object
	return target;
};

源码中常见的pushStack

该函数用于创建一个新的jQuery对象,然后将一个DOM元素集合加入到jQuery栈中,最后返回该jQuery对象,有三个参数,如下:

  elems    Array类型 将要压入 jQuery 栈的数组元素,用于生成一个新的 jQuery 对象

  name    可选。 String类型 生成数组元素的 jQuery 方法名

  selector   可选。 Array类型 传递给 Query 方法的参数(用于序列化)

参数2和参数3可选的,用于设置返回的新的jQuery对象的selector属性

调用pushStack后之前的jQuery对象内的DOM引用是不会消失的,还保存到新的对象的prevObject里,我们可以通过end()来获取之前的jQuery对象

下面是源码:

pushStack: function( elems ) {

		// Build a new jQuery matched element set
		var ret = jQuery.merge( this.constructor(), elems );

		// Add the old object onto the stack (as a reference)
		ret.prevObject = this;
		ret.context = this.context;

		// Return the newly-formed element set
		return ret;
	}

jQuery源码分页注释

盗的图↓↓↓