前端开发系列076-JQuery篇之框架源码解读[插件]

这篇文章将主要介绍jQuery框架的插件机制,包括但不限于jQuery.extendjQuery.fn.extend方法的设计和使用,JavaScript体系中的常用概念以及jQuery插件的使用等。

一、源码解读

jQuery.extend = jQuery.fn.extend = function() {
	//声明一堆的变量length 为实参的个数
	var options, name, src, copy, copyIsArray, 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" && !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 = Array.isArray( copy ) ) ) ) {

					if ( copyIsArray ) {
						copyIsArray = false;
						clone = src && Array.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;
};

//为jQuery添加一堆工具方法,对象参数中所有的成员都将直接添加在jQuery函数身上成为jQuery的静态方法
jQuery.extend( {

	// Unique for each copy of jQuery on the page
	// 简单测试下得到的结果是:jQuery33104605303773584173
	// 确保页面中的jQuery副本是唯一的,(jQuery + 版本号 + 随机数) => 格式处理
	expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),

	// Assume jQuery is ready without the ready module
	// 是否已经准备就绪
	isReady: true,
	//错误处理方法:根据传入的消息创建一个错误对象并抛出异常
	error: function( msg ) {
		throw new Error( msg );
	},
	//空函数
	noop: function() {},

	//用于判断指定参数是否是一个纯粹的对象
	//所谓"纯粹的对象",就是该对象是通过"{}"或"new Object"创建的 .排除了数组、自定义构造函数创建的对象以及函数等类型
	isPlainObject: function( obj ) {
		var proto, Ctor;

		// Detect obvious negatives
		// Use toString instead of jQuery.type to catch host objects

		//如果参数为null | undefined 或者在调用Object.prototype.toString.call(参数)的时候得到的结果不是[object Object]则直接返回false
		if ( !obj || toString.call( obj ) !== "[object Object]" ) {
			return false;
		}

		//获取当前对象的原型对象,其实是调用了Object.getPrototypeOf(参数) 方法
		proto = getProto( obj );

		// Objects with no prototype (e.g., `Object.create( null )`) are plain
		//监测 没有原型对象的最纯净的对象 例如使用Object.create( null )创建的对象

		if ( !proto ) {
			return true;
		}

		// Objects with prototype are plain iff they were constructed by a global Object function
		//如果原型对象上拥有constructor属性(前一句的结果为true) 那么就返回proto.constructor的值
		//如果原型对象是Object.prototype 那么Object.prototype.constructor ==> ƒ Object() { [native code] }
		Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;

		//检查Ctor是否是函数 且函数字符串是否全等于function Object() { [native code] }
		return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
	},

	//用于判断指定参数是否是一个空对象
	//所谓"空对象",即不包括任何可枚举(自定义)的属性。简而言之,就是该对象没有属性可以通过for...in迭代。
	isEmptyObject: function( obj ) {

		/* eslint-disable no-unused-vars */
		// See https://github.com/eslint/eslint/issues/6125
		var name;

		for ( name in obj ) {
			return false;
		}
		return true;
	},

	// Evaluates a script in a global context
	//用于全局性地执行一段JavaScript代码,内部调用DOMEval方法实现
	//其作用与常规的JavaScript eval()函数相似。区别自傲与jQuery.globalEval()执行代码的作用域为全局作用域。
        //使用示例:$.globalEval( "var a =1" ); 该行代码将在全局作用域中定义变量a
	globalEval: function( code ) {
		DOMEval( code );
	},

	//常用的迭代方法,可以用来遍历数组|对象|jQ实例对象(伪数组)
	//该方法同$("xxx").each() 方法保持一致
	//第一个参数:要遍历的对象| 数组 | jQ实例对象
	each: function( obj, callback ) {
		var length, i = 0;

		//如果参数是伪数组那么使用普通的for循环来进行遍历
		if ( isArrayLike( obj ) ) {
			length = obj.length;
			for ( ; i < length; i++ ) {
				//调用回到函数,把当前的value值绑定给函数的this [each方法的回调函数中this--> value值]
				//把当前循环的key和value值(这里是i和obj[i])作为参数传递给callback回调函数
				//检查回调函数的返回值,如果返回的是false,那么就退出循环
				if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
					break;
				}
			}
		} else {
			//如果是普通的对象那么使用for..in循环来进行遍历
			for ( i in obj ) {
				//同上面的代码保持一致
				if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
					break;
				}
			}
		}

		//把遍历的对象返回,链式编程的代码风格
		return obj;
	},

	// Support: Android <=4.0 only
	// 工具方法,用于清空字符串前面或者是后面的N个空格
	// 在ES5中js提供了原生的trim方法来清除字符串前后的1个或多个空格
	// 这里主要是通过正则表达式去进行匹配,把匹配到的内容替换为空字符串""
	trim: function( text ) {
		return text == null ?
			"" :
			( text + "" ).replace( rtrim, "" );
	},

	// results is for internal usage only
	// 结果仅共内部使用
	// 该函数用于将一个类数组对象(伪数组)转换为真正的数组对象
	// 所谓"类数组对象"就是一个常规的Object对象,但它和数组对象非常相似:具备length属性,并以0、1、2、3……等数字作为属性名。
	makeArray: function( arr, results ) {
		// 初始化ret为空数组
		var ret = results || [];

		if ( arr != null ) {
			//检查传入的参数是否是伪数组
			if ( isArrayLike( Object( arr ) ) ) {
				//如果是伪数组,那么合并ret和arr 并返回
				//如果arr是字符串那么jQuery.merge(ret,[arr]) ,否则jQuery.merge(ret,arr)
				jQuery.merge( ret,
					typeof arr === "string" ?
					[ arr ] : arr
				);
			} else {
				//如果参数不是伪数组,那么直接把arr中的每个数据都添加到新的数组中
				//push.call( ret, arr ) 就是是 [].push.call(ret,arr) => [].push(arr)
				push.call( ret, arr );
			}
		}
		//返回处理完的数组对象
		return ret;
	},

	// 该方法用于在数组中搜索指定的值,并返回其索引值。如果数组中不存在该值,则返回 -1。
	// 第一个参数 用于查找的值
	// 第二个参数 指定被查找的数组
	// 第三个参数 指定从数组的指定索引位置开始查找,默认为 0
        // 如果数组中存在多个相同的值,则以查找到的第一个值的索引为准
	// 使用示例:$.inArray("文顶顶",["demoA","demoB","wendingding.com","文顶顶","demoC","end"],4)
	// 上面的代码表示从数组中索引为4的位置开始查找"文顶顶"这个元素项,返回的结果为-1,最后一个参数不传递则返回3
	inArray: function( elem, arr, i ) {
		//如果参数是null或undefined那么直接返回-1
		//否则通过调用indexOf方法实现 indexOf.call( arr, elem, i ) => arr.indexOf(elem,i)
		return arr == null ? -1 : indexOf.call( arr, elem, i );
	},

	// Support: Android <=4.0 only, PhantomJS 1 only
	// push.apply(_, arraylike) throws on ancient WebKit
	//该方法用于合并两个数组
	merge: function( first, second ) {
		var len = +second.length,
			j = 0,
			i = first.length;

		for ( ; j < len; j++ ) {
			//通过普通的for循环来遍历第二个数组
			//把第二个数组的元素依次追加在第一个数组的后面
			first[ i++ ] = second[ j ];
		}

		//更新数组的长度值
		first.length = i;

		return first;
	},

	// 过滤函数用于过滤数组
	// 参数1 待过滤的数组
	// 参数2 过滤数组的具体函数
	// 参数3 布尔类型的值
	// 		为true则函数返回数组中由过滤函数返回 true 的元素
	//      为false则函数返回数组中由过滤函数返回false的元素
	grep: function( elems, callback, invert ) {

		//初始化一堆的变量
		var callbackInverse,
			matches = [],   //空数组
			i = 0,			//索引值为0
			length = elems.length,	//待过滤数组的长度
			callbackExpect = !invert;

		// Go through the array, only saving the items
		// that pass the validator function
		for ( ; i < length; i++ ) {
			//每循环一次就把当前的元素和对应的索引传递给回调函数,并保存回调函数的返回值取反
			callbackInverse = !callback( elems[ i ], i );
			if ( callbackInverse !== callbackExpect ) {
				//把过滤后的元素收集保存到新的数组中
				matches.push( elems[ i ] );
			}
		}

		//返回过滤后得到的数据,是一个新的数组
		return matches;
	},

	// arg is for internal usage only
	// arg仅用于内部使用的情况
	// 数组映射方法( 将一个数组中的元素转换到另一个数组中 )
	// 参数1 :待处理的数组
	// 参数2 : 具体的处理函数
	// 参数3 : arg
	map: function( elems, callback, arg ) {

		//初始化一堆的变量
		var length, value,
			i = 0,			//索引值为0
			ret = [];		//ret为空的数组

		// Go through the array, translating each of the items to their new values
		// 遍历数组把数组中的每一项都转换为一个新的值

		//检查是否是伪数组
		if ( isArrayLike( elems ) ) {
			length = elems.length;		//获取待处理的伪数组的长度
			for ( ; i < length; i++ ) {
				//循环,每循环一次就调用处理函数并把当前的key和value值作为参数传递进去
				//收集回调函数的返回值
				value = callback( elems[ i ], i, arg );

				//如果回调函数的范返回值不为空,那么就把该返回值添加到数组中并最终返回
				if ( value != null ) {
					ret.push( value );
				}
			}

		// Go through every key on the object,
		//	如果是普通的对象,那么就使用for...in循环来进行遍历
		} else {
			for ( i in elems ) {
				value = callback( elems[ i ], i, arg );

				if ( value != null ) {
					ret.push( value );
				}
			}
		}

		// Flatten any nested arrays
		// 等价于 [].concat(ret) 问题:为什么不直接范湖ret数组呢?
		return concat.apply( [], ret );
	},

	// A global GUID counter for objects
	// 全局的GUID计数器
	guid: 1,

	// jQuery.support is not used in Core but other projects attach their
	// properties to it so it needs to exist.
        // 就是个空对象 {} 不在核心中使用,但其他项目将它们的属性附加到它,因此它需要存在。
	support: support
} );

二、jQuery框架的插件处理机制

jQuery框架中上面列出的这几百行代码主要做了两件事情。

>❏ 在jQuery的基础上拓展了`jQuery.extend`方法和`jQuery.fn.extend`方法。
>❏ 调用`jQuery.extend`方法来为jQuery批量的添加一堆的静态方法(如`map`和`grep`等)。

jQuery插件方法的实现

jQuery官方框架中在实现jQuery.extendjQuery.fn.extend这两个方法的时候用了接近80行代码,具体的实现细节比较复杂,这里给出一个简易版本来帮助大家理解这两个方法都做了什么事情。

jQuery.extend = jQuery.fn.extend = function (objT) {
    //遍历参数对象,获取对象中的每个成员添加在jQuery或者是jQuery.fn对象。
 	for (var key in objT)
 		{
 		    // 过滤掉原型对象的成员,如果仅仅只处理函数还可以对参数的类型进行判断
 			if (Object.hasOwnProperty(key))
 			{
 			    //this的值由函数的调用方式决定
			    this[key] = objT[key];
 			}
 		}
     };

其实简单点说,这两个的方法的功能就是把传递给他们的参数(对象类型)中的所有成员都添加到对应的对象中(如果是jQuery.fn.extend方法,那么就添加到jQuery的原型对象身上作为原型成员来使用,如果是jQuery.extend方法,则直接添加到jQuery身上作为静态方法来使用)。

在这两个方法的实现中巧妙利用了this指向的对象由函数调用方式决定这一特点,做到jQuery.extend = jQuery.fn.extend。也因此,虽然在jQuery.extend方法jQuery.fn.extend方法的函数体相同,但由于内部使用了this的缘故,所以在单独调用这两个方法的时候它们是区分处理的。

jQuery插件方法的调用

这里给出上面代码中jQuery.extend()调用的简化版本。

jQuery.extend({
    //判断指定参数是否是一个纯粹的对象
    isPlainObject:function(obj){},

    //判断指定参数是否是一个空的对象
    isEmptyObject:function(obj){},

    //迭代方法,用来遍历对象、数组和伪数组
    each:function( obj, callback){},

    //清除字符串前后的N个空格,同ES5中字符串的trim方法
    trim:function(text){},

    //把伪数组对象转换为数组
    makeArray:function(arr, results ){},

    //该方法用于在数组中搜索指定的值,并返回其索引值。
    inArray:function(elem, arr, i) {},

    //该方法用于合并两个数组
    merge:function(first, second ){},

    //这是一个过滤函数,主要用于过滤数组
    grep: function( elems, callback, invert ) {}

    //数组映射方法,把一个数组中的元素转换到另一个数组中
    map:function( elems, callback, arg ) {}

})

jQuery.extend() 和jQuery.fn.extend() 这两个方法在jQuery框架中被大量使用,绝大多数的方法均通过调用者两个函数来添加到jQuery身上或者是jQuery的原型对象身上,这种为jQuery和jQuery原型对象拓展方法(功能)的机制被称为插件机制

jQuery框架的官方团队维护了一个jQuery官方插件列表,我们可以在使用jQuery的基础上通过引入对应的jQuery插件来快速实现特定的功能。当然,除jQuery官方团队维护的插件之外,我们还能在互联网上找到很多其它开发人员为jQuery写的一些优秀插件,在具体的开发中可以根据项目的需求来综合评估是否直接使用jQuery插件来实现。

总体来说,jQuery的插件实现是比较简单的。如果你想要自己写一个jQuery插件,那么只需要创建一个新的js文件,该文件推荐命名为jQuery.xx.js的形式。然后在js代码中通过调用jQuery.extend() 和jQuery.fn.extend() 方法来拓展功能即可。

常见的插件实现形式是:把对应的代码包裹到闭包中,然后传递jQuery对象。

posted on 2022-12-15 08:56  文顶顶  阅读(83)  评论(0编辑  收藏  举报

导航