前端开发系列076-JQuery篇之框架源码解读[插件]
这篇文章将主要介绍jQuery框架的插件机制,包括但不限于
jQuery.extend
和jQuery.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.extend
和jQuery.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()
方法来拓展功能即可。