jquery ui widget原理解析

郜彦
2023-12-01

jquery ui widget提供了对jquery ui的widget进行创建和使用的基本运行框架和接口规范。其主要作用是对定义的widget组件提供组件注册和继承机制的功能。

在jquery ui widget主要提供了$.widget和$.Widget两个函数对象。一个小写一个大写。

$.widget提供了对widget组件类的构造函数的创建,实现对widget类的完整属性的封装和注册功能以及继承机制。查看jquery ui的源码可以看到,jquery ui中的所有自定义widget组件都是使用$.widget函数创建的,使用$.widget函数就会生成该widget组件类的构造函数,然后使用$.widget.bridge函数来将自定义的widget组件类注册到jQuery对象上,然后在jQuery对象上就可以使用该方法了,可以使用jQuery(element).xxWidget()的方法进行调用了,其中xxWidget就是widget的name,这样就完成了对jQuery对象的扩展。一个自定义的widget类利用$.widget函数只需要实现特定的接口方法和指定widget自己私有和公有属性和方法后即可。$.widget负责对于这些函数的调用以及属性的继承机制的实现。至于需要实现哪些接口方法,这些就是$.Widget(注意是大写的W)函数对象的作用了。

$.Widget是所有jquery ui中widget组件的基类,就和javascript所有对象的基类是Object一样。$.Widget里面定义了jquery ui中定义的组件的一些基本属性和接口方法。例如里面有一些基本的属性有:

	widgetName: "widget",
	widgetEventPrefix: "",
	defaultElement: "<div>",
	options: {
		disabled: false,

		// callbacks
		create: null
	}
jquery ui中每个定义的widget中都有这些属性,也可以进行重写。

还有一些定义的widget需要实现的方法,包括:_getCreateOptions, _getCreateEventData, _create, _init, _destroy等。当然这里面有些函数可以不实现,一般情况下,_create函数是必须实现的,其余的函数可以根据需要而定。

好了,基本的原理就是这样。下面分别对$..widget和$.Widget的源码进行解析。这里以jQuery UI Widget 1.11.4版本为例。

首先看一下$..widget。

$.widget = function( name, base, prototype ) {
	var fullName, existingConstructor, constructor, basePrototype,
		// proxiedPrototype allows the provided prototype to remain unmodified
		// so that it can be used as a mixin for multiple widgets (#8876)
		proxiedPrototype = {},
	//一般的widget在定义的时候,都是xxx.xxxx的形式,分别表示namespace和widgetName
	//例如ui.tab,namespace='ui';name='tab';fullName='ui-tab';
		namespace = name.split( "." )[ 0 ];
	
	name = name.split( "." )[ 1 ];
	fullName = namespace + "-" + name;
	
	//如果prototype没有指定,那么就以$.Widget作为基类
	if ( !prototype ) {
		prototype = base;
		base = $.Widget;
	}

	//$.expr.pseudos === $.expr[":"] 用来创建自定义的伪类选择器,可以直接使用fullName选择出已经创建该plugin的element
	//关于$.expr.pseudos原理请见http://www.tuicool.com/articles/NzYRZvq
	// create selector for plugin
	$.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) {
		return !!$.data( elem, fullName );
	};

	$[ namespace ] = $[ namespace ] || {};
	existingConstructor = $[ namespace ][ name ];
	constructor = $[ namespace ][ name ] = function( options, element ) {
		//加这句代码的意思就是允许不用new constructor()的形式,而是可以用constructor()直接创建plugin的对象
		//因为$.widget函数返回的就是constructor函数
		//因为直接用constructor()创建的话,this就可能不是当前正在创建的对象,例如可能是window对象。这样this上就不会有createWidget函数
		//因此在这里面重新调用new constructor()函数来将this指针指向为当前的对象
		//因为所有的jquery ui的widget都是继承$.Widget,所以此时的this肯定就会有createWidget了,所以不会死循环
		//对于javascript new的原理可以参考http://www.cnblogs.com/purediy/archive/2012/09/12/2682490.html
		//以及javascript原型原理http://www.cnblogs.com/wilber2013/p/4924309.html
		// allow instantiation without "new" keyword
		if ( !this._createWidget ) {
			return new constructor( options, element );
		}

		// allow instantiation without initializing for simple inheritance
		// must use "new" keyword (the code above always passes args)
		if ( arguments.length ) {
			this._createWidget( options, element );
		}
	};
	//也许之前已经定义过该plugin,这里将已有的构造函数的属性拷贝到现在新的构造函数上
	// extend with the existing constructor to carry over any static properties
	$.extend( constructor, existingConstructor, {
		version: prototype.version,
		// copy the object used to create the prototype in case we need to
		// redefine the widget later
		_proto: $.extend( {}, prototype ),
		// track widgets that inherit from this widget in case this widget is
		// redefined after a widget inherits from it
		_childConstructors: []
	});
	
	//创建父类的空对象,这样父类中所有的属性都是默认值,因而可以实现属性的继承
	basePrototype = new base();
	// we need to make the options hash a property directly on the new instance
	// otherwise we'll modify the options hash on the prototype that we're
	// inheriting from
	basePrototype.options = $.widget.extend( {}, basePrototype.options );
	$.each( prototype, function( prop, value ) {
		if ( !$.isFunction( value ) ) {
			proxiedPrototype[ prop ] = value;
			return;
		}
		//对该widget内部定义的方法都进行了代理类的封装
		proxiedPrototype[ prop ] = (function() {
			var _super = function() {
					return base.prototype[ prop ].apply( this, arguments );
				},
				_superApply = function( args ) {
					return base.prototype[ prop ].apply( this, args );
				};
			return function() {
				var __super = this._super,
					__superApply = this._superApply,
					returnValue;

				this._super = _super;
				this._superApply = _superApply;

				returnValue = value.apply( this, arguments );

				this._super = __super;
				this._superApply = __superApply;

				return returnValue;
			};
		})();
	});
	constructor.prototype = $.widget.extend( basePrototype, {
		// TODO: remove support for widgetEventPrefix
		// always use the name + a colon as the prefix, e.g., draggable:start
		// don't prefix for widgets that aren't DOM-based
		//widgetEventPrefix为name 这样该widget的所有事件就会widgetEventPrefix+event,
		//使用bind监听该widget的事件时,需要bind(widgetEventPrefix+event...)
		widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name  
	}, proxiedPrototype, {
		constructor: constructor,  //每个函数的原型都需要有constructor属性来指向构造函数本身
		namespace: namespace,
		widgetName: name,
		widgetFullName: fullName
	});
	
	//在重新定义了父类的构造函数后,所有继承该类的子类的构造函数都要重新定义
	// If this widget is being redefined then we need to find all widgets that
	// are inheriting from it and redefine all of them so that they inherit from
	// the new version of this widget. We're essentially trying to replace one
	// level in the prototype chain.
	if ( existingConstructor ) {
		$.each( existingConstructor._childConstructors, function( i, child ) {
			var childPrototype = child.prototype;

			// redefine the child widget using the same prototype that was
			// originally used, but inherit from the new version of the base
			$.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto );
		});
		// remove the list of existing child constructors from the old constructor
		// so the old child constructors can be garbage collected
		delete existingConstructor._childConstructors;
	} else {
		base._childConstructors.push( constructor );
	}
	//将自定义的plugin注册到jQuery对象上,这样就可以使用jQuery创建对象的方式进行plugin对象的创建
	$.widget.bridge( name, constructor );

	return constructor;
};

这里面用到的bridge函数正如上面所说,将widget注册在jQuery对象上,在bridge函数内部创建了$.fn[name]函数。函数源码如下:

$.widget.bridge = function( name, object ) {
	var fullName = object.prototype.widgetFullName || name;
	//使用widget的name将widget注册在jQuery对象上
	$.fn[ name ] = function( options ) {
		var isMethodCall = typeof options === "string",
			args = widget_slice.call( arguments, 1 ),
			//returnValue首先赋值为s,即当前的jQuery实例对象,是为了统一jquery的链式操作
			returnValue = this;

		//如果传进来的参数是string,说明是调用widget内部的指定的函数
		if ( isMethodCall ) {
			this.each(function() {
				var methodValue,
					instance = $.data( this, fullName );
				//如果传进来的参数是"instance",直接返回在该元素上创建的该widget实例
				if ( options === "instance" ) {
					returnValue = instance;
					return false;
				}
				if ( !instance ) {
					return $.error( "cannot call methods on " + name + " prior to initialization; " +
						"attempted to call method '" + options + "'" );
				}
				//当该参数指定的函数不存在,或者是以“_”开头,说明是内部私有函数,不能调用
				if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) {
					return $.error( "no such method '" + options + "' for " + name + " widget instance" );
				}
				methodValue = instance[ options ].apply( instance, args );
				if ( methodValue !== instance && methodValue !== undefined ) {
					returnValue = methodValue && methodValue.jquery ?  //说明methodValue是一个JQuery实例对象
					//使用pushStack重新把returnValue变成一个jQuery实例对象,
					//methodValue本来就是个jQuery对象,为什么不直接把returnValue直接赋值给returnValue呢
					//这是因为链式操作中,可能methodValue的prevObject不是当前的this对象,
					//这样如果进行end()操作就不会回退到当前的this jQuery实例对象,破坏了操作的延续性
					//而pushStack的作用就是将传入的dom元素集合压入一个新的JQuery对象实例中,
					//并把这个新的JQuery对象实例的prevObject赋值为pushStack函数的调用者
					returnValue.pushStack( methodValue.get() ) : //methodValue.get()返回dom元素数组
						methodValue;
					return false;
				}
			});
		} else {

			// Allow multiple hashes to be passed on init
			if ( args.length ) {
				options = $.widget.extend.apply( null, [ options ].concat(args) );
			}

			this.each(function() {
				var instance = $.data( this, fullName );
				//如果该元素上已经创建该widget实例对象,就重新设置option参数,并调用_init函数
				if ( instance ) {
					instance.option( options || {} );
					if ( instance._init ) {
						instance._init();
					}
				} else {
					//调用构造函数创建该widget的实例对象,并且把对象存入该dom元素上
					$.data( this, fullName, new object( options, this ) );
				}
			});
		}

		return returnValue;
	};
};

好了,$.widget的原理基本上解析完了,下面再来看看jquery ui所有widget的基类$.Widget

//空的构造函数
$.Widget = function( /* options, element */ ) {};
//用于记录继承该类的子类
$.Widget._childConstructors = [];

//每个函数对象都有原型对象,可以实现继承机制
//这里面包括了widget所具有的一些基本属性和方法。所有的jquery ui widget都会拥有这些属性和方法
$.Widget.prototype = {
	widgetName: "widget",
	widgetEventPrefix: "",
	defaultElement: "<div>",
	options: {
		disabled: false,

		// callbacks
		create: null
	},
	//真正的初始化方法,每个widget都会调用该方法来执行初始化创建
	_createWidget: function( options, element ) {
		element = $( element || this.defaultElement || this )[ 0 ];
		this.element = $( element );
		this.uuid = widget_uuid++;
		this.eventNamespace = "." + this.widgetName + this.uuid;

		this.bindings = $();
		this.hoverable = $();
		this.focusable = $();

		if ( element !== this ) {
			$.data( element, this.widgetFullName, this );
			//使用Widget的_on函数注册remove事件处理函数,第一个参数为true,表示不禁用该事件响应函数
			this._on( true, this.element, {
				remove: function( event ) {
					if ( event.target === element ) {
						this.destroy();
					}
				}
			});
			this.document = $( element.style ?
				// element within the document
				element.ownerDocument :
				// element is window or document
				element.document || element );
			this.window = $( this.document[0].defaultView || this.document[0].parentWindow );
		}

		this.options = $.widget.extend( {},
			this.options,
			this._getCreateOptions(),
			options );
		//这里调用的_create函数执行具体的初始化创建操作
		this._create();
		this._trigger( "create", null, this._getCreateEventData() );
		this._init();
	},
	_getCreateOptions: $.noop,
	_getCreateEventData: $.noop,
	_create: $.noop,
	_init: $.noop,

	destroy: function() {
		this._destroy();
		// we can probably remove the unbind calls in 2.0
		// all event bindings should go through this._on()
		this.element
			.unbind( this.eventNamespace )
			.removeData( this.widgetFullName )
			// support: jquery <1.6.3
			// http://bugs.jquery.com/ticket/9413
			.removeData( $.camelCase( this.widgetFullName ) );
		this.widget()
			.unbind( this.eventNamespace )
			.removeAttr( "aria-disabled" )
			.removeClass(
				this.widgetFullName + "-disabled " +
				"ui-state-disabled" );

		// clean up events and states
		this.bindings.unbind( this.eventNamespace );
		this.hoverable.removeClass( "ui-state-hover" );
		this.focusable.removeClass( "ui-state-focus" );
	},
	_destroy: $.noop,

	widget: function() {
		return this.element;
	},

	//获取或设置特定的option value.支持使用“xx.xxx”的形式来操作深层次的对象
	option: function( key, value ) {
		var options = key,
			parts,
			curOption,
			i;
		
		//直接调用option(),不指定参数,就直接返回该widget的options对象
		if ( arguments.length === 0 ) {
			// don't return a reference to the internal hash
			return $.widget.extend( {}, this.options );
		}

		if ( typeof key === "string" ) {
			// handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
			options = {};
			parts = key.split( "." );
			key = parts.shift();
			if ( parts.length ) {
				curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
				for ( i = 0; i < parts.length - 1; i++ ) {
					curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
					curOption = curOption[ parts[ i ] ];
				}
				key = parts.pop();
				if ( arguments.length === 1 ) {
					return curOption[ key ] === undefined ? null : curOption[ key ];
				}
				curOption[ key ] = value;
			} else {
				if ( arguments.length === 1 ) {
					return this.options[ key ] === undefined ? null : this.options[ key ];
				}
				options[ key ] = value;
			}
		}

		this._setOptions( options );

		return this;
	},
	_setOptions: function( options ) {
		var key;

		for ( key in options ) {
			this._setOption( key, options[ key ] );
		}

		return this;
	},
	_setOption: function( key, value ) {
		this.options[ key ] = value;

		if ( key === "disabled" ) {
			this.widget()
				.toggleClass( this.widgetFullName + "-disabled", !!value );

			// If the widget is becoming disabled, then nothing is interactive
			if ( value ) {
				this.hoverable.removeClass( "ui-state-hover" );
				this.focusable.removeClass( "ui-state-focus" );
			}
		}

		return this;
	},

	enable: function() {
		return this._setOptions({ disabled: false });
	},
	disable: function() {
		return this._setOptions({ disabled: true });
	},

	//事件监听注册函数 suppressDisabledCheck表示是否禁止该事件禁用的检查
	_on: function( suppressDisabledCheck, element, handlers ) {
		var delegateElement,
			instance = this;

		// no suppressDisabledCheck flag, shuffle arguments
		if ( typeof suppressDisabledCheck !== "boolean" ) {
			handlers = element;
			element = suppressDisabledCheck;
			suppressDisabledCheck = false;
		}

		// no element argument, shuffle and use this.element
		if ( !handlers ) {
			handlers = element;
			element = this.element;
			delegateElement = this.widget();
		} else {
			element = delegateElement = $( element );
			this.bindings = this.bindings.add( element );
		}

		$.each( handlers, function( event, handler ) {
			function handlerProxy() {
				// allow widgets to customize the disabled handling
				// - disabled as an array instead of boolean
				// - disabled class as method for disabling individual parts
				if ( !suppressDisabledCheck && //如果对该事件的禁用与否进行检查且该instance的disable为true,那么就不会响应该事件,响应函数不起作用
						( instance.options.disabled === true ||
							$( this ).hasClass( "ui-state-disabled" ) ) ) {
					return;
				}
				return ( typeof handler === "string" ? instance[ handler ] : handler )
					.apply( instance, arguments );
			}

			// copy the guid so direct unbinding works
			if ( typeof handler !== "string" ) {
				handlerProxy.guid = handler.guid =
					handler.guid || handlerProxy.guid || $.guid++;
			}

			//对事件监听函数进行绑定。传进来的event参数可能是“eventName selector”的形式
			var match = event.match( /^([\w:-]*)\s*(.*)$/ ),
				eventName = match[1] + instance.eventNamespace,
				selector = match[2];
			if ( selector ) {
				//如果有selector则表示对delegateElement的某子元素上的事件进行监听并注册事件响应函数
				delegateElement.delegate( selector, eventName, handlerProxy );
			} else {
				element.bind( eventName, handlerProxy );
			}
		});
	},
	
	//取消事件的监听
	_off: function( element, eventName ) {
		eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) +
			this.eventNamespace;
		element.unbind( eventName ).undelegate( eventName );

		// Clear the stack to avoid memory leaks (#10056)
		this.bindings = $( this.bindings.not( element ).get() );
		this.focusable = $( this.focusable.not( element ).get() );
		this.hoverable = $( this.hoverable.not( element ).get() );
	},
	
	//延迟调用函数
	_delay: function( handler, delay ) {
		function handlerProxy() {
			return ( typeof handler === "string" ? instance[ handler ] : handler )
				.apply( instance, arguments );
		}
		var instance = this;
		return setTimeout( handlerProxy, delay || 0 );
	},

	//增加鼠标悬停事件响应函数
	_hoverable: function( element ) {
		this.hoverable = this.hoverable.add( element );
		this._on( element, {
			mouseenter: function( event ) {
				$( event.currentTarget ).addClass( "ui-state-hover" );
			},
			mouseleave: function( event ) {
				$( event.currentTarget ).removeClass( "ui-state-hover" );
			}
		});
	},
	
	//增加焦点事件响应函数
	_focusable: function( element ) {
		this.focusable = this.focusable.add( element );
		this._on( element, {
			focusin: function( event ) {
				$( event.currentTarget ).addClass( "ui-state-focus" );
			},
			focusout: function( event ) {
				$( event.currentTarget ).removeClass( "ui-state-focus" );
			}
		});
	},

	//触发事件函数 type表示事件的类型,event表示具体的事件对象,可为空
	_trigger: function( type, event, data ) {
		var prop, orig,
			callback = this.options[ type ]; //直接获取该类型的事件的回调函数

		data = data || {};
		event = $.Event( event );
		event.type = ( type === this.widgetEventPrefix ?
			type :
			this.widgetEventPrefix + type ).toLowerCase();
		// the original event may come from any element
		// so we need to reset the target on the new event
		event.target = this.element[ 0 ];

		// copy original event properties over to the new event
		orig = event.originalEvent;
		if ( orig ) {
			for ( prop in orig ) {
				if ( !( prop in event ) ) {
					event[ prop ] = orig[ prop ];
				}
			}
		}

		this.element.trigger( event, data );   //触发事件
		return !( $.isFunction( callback ) &&
			callback.apply( this.element[0], [ event ].concat( data ) ) === false ||
			event.isDefaultPrevented() );
	}
};



 类似资料: