jquery ui widget 源代碼分析

jquery ui 的所有組件都是基於一個簡單,可重用的widget。javascript

這個widget是jquery ui的核心部分,有用它能實現一致的API。建立有狀態的插件,而無需關心插件的內部轉換。java

$.widget( name, base, prototype )jquery

widget一共同擁有2或3個參數。base爲可選。設計模式

這裏之因此把base放在第二個參數裏,主要是因爲這樣寫代碼更直觀一些。(因爲後面的prototype 是個代碼很長的大對象)。api

name:第一個參數是一個包括一個命名空間和組件名稱的字符串,經過」.」來切割。數組


命名空間必須有。它指向widget prototype存儲的全局jQuery對象。緩存


假設命名空間沒有,widget factory將會爲你生成。app

widget name是插件函數和原型的真實名稱,
比方: jQuery.widget( 「demo.multi」, {…} ) 將會生成 jQuery.demo , jQuery.demo.multi , and jQuery.demo.multi.prototype .dom

base:第二個參數(可選)是 widget prototype繼承於什麼對象。ide


好比jQuery UI有一個「mouse」的插件,它可以做爲其它的插件提供的基礎。
爲了實現這個所有的基於mouse的插件比方draggable,
droppable可以這麼作: jQuery.widget( "ui.draggable", $.ui.mouse, {...} );
假設沒有這個參數。widget默認繼承自「base widget」 jQuery.Widget(注意jQuery.widget 和 jQuery.Widget不一樣) 。

prototype:最後一個參數是一個對象文字。它會轉化爲所有widget實例的prototype。widget factory會生成屬性鏈,鏈接到她繼承的widget的prototype。一直到最主要的 jQuery.Widget。

一旦你調用jQuery.widget。它會在jQuery prototype ( jQuery.fn )上生成一個新的可用方法相應於widget的名字。比方咱們這個樣例jQuery.fn.multi。

.fn方法是包括Dom元素的jquery對象和你生成的 widget prototyp實例的接口。爲每一個jQuery對象生成一個新的widget的實例。

/*!
 * jQuery UI Widget @VERSION
 * http://jqueryui.com
 *
 * Copyright 2014 jQuery Foundation and other contributors
 * Released under the MIT license.
 * http://jquery.org/license
 *
 * http://api.jqueryui.com/jQuery.widget/
 */

//這裏斷定是否支持amd or cmd 模式
(function(factory) {
	if (typeof define === "function" && define.amd) {

		// AMD. Register as an anonymous module.
		define(["jquery"], factory);
	} else {

		// Browser globals
		factory(jQuery);
	}
}(function($) {

	var widget_uuid = 0,
		//插件的實例化數量
		widget_slice = Array.prototype.slice; //數組的slice方法,這裏的做用是將參賽arguments 轉爲真正的數組

	//清除插件的數據及緩存
	$.cleanData = (function(orig) {
		return function(elems) {
			for (var i = 0, elem;
			(elem = elems[i]) != null; i++) {
				try {
					// 重寫cleanData方法,調用後觸發每個元素的remove事件
					$(elem).triggerHandler("remove");
					// http://bugs.jquery.com/ticket/8235
				} catch (e) {}
			}
			orig(elems);
		};
	})($.cleanData);

	/**
	 * widget工廠方法。用於建立插件
	 * @param name 包括命名空間的插件名稱。格式 xx.xxx
	 * @param base 需要繼承的ui組件
	 * @param prototype 插件的實際代碼
	 * @returns {Function}
	 */
	$.widget = function(name, base, prototype) {
		var fullName, //插件全稱
		existingConstructor, //原有的構造函數
		constructor, //當前構造函數
		basePrototype, //父類的Prototype
		// proxiedPrototype allows the provided prototype to remain unmodified
		// so that it can be used as a mixin for multiple widgets (#8876)
		proxiedPrototype = {},
			//可調用父類方法_spuer的prototype對象,擴展於prototype
			namespace = name.split(".")[0];

		name = name.split(".")[1];
		fullName = namespace + "-" + name;
		//假設僅僅有2個參數  base默以爲Widget類,組件默認會繼承base類的所有方法
		if (!prototype) {
			prototype = base;
			base = $.Widget;
		}

		//    console.log(base, $.Widget)

		// create selector for plugin
		//建立一個本身定義的僞類選擇器
		//如 $(':ui-menu') 則表示選擇定義了ui-menu插件的元素
		$.expr[":"][fullName.toLowerCase()] = function(elem) {
			return !!$.data(elem, fullName);
		};

		// 斷定命名空間對象是否存在,沒有的話 則建立一個空對象
		$[namespace] = $[namespace] || {};
		//這裏存一份舊版的插件,假設這個插件已經被使用或者定義了
		existingConstructor = $[namespace][name];
		//這個是插件實例化的主要部分
		//constructor存儲了插件的實例,同一時候也建立了基於命名空間的對象
		//如$.ui.menu
		constructor = $[namespace][name] = function(options, element) {
			// allow instantiation without "new" keyword
			//贊成直接調用命名空間上的方法來建立組件
			//比方:$.ui.menu({},'#id') 這樣的方式建立的話,默認沒有new 實例化。

因爲_createWidget是prototype上的方法,需要new關鍵字來實例化 //經過 調用 $.ui.menu 來實例化插件 if (!this._createWidget) { console.info(this) return new constructor(options, element); } // allow instantiation without initializing for simple inheritance // must use "new" keyword (the code above always passes args) //假設存在參數,則說明是正常調用插件 //_createWidget是建立插件的核心方法 if (arguments.length) { this._createWidget(options, element); } }; // extend with the existing constructor to carry over any static properties //合併對象。將舊插件實例,及版本、prototype合併到constructor $.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: [] }); //實例化父類 獲取父類的 prototype 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 //這裏深複製一份options basePrototype.options = $.widget.extend({}, basePrototype.options); //在傳入的ui原型中有方法調用this._super 和this.__superApply會調用到base上(最基類上)的方法 $.each(prototype, function(prop, value) { //假設val不是function 則直接給對象賦值字符串 if (!$.isFunction(value)) { proxiedPrototype[prop] = value; return; } //假設val是function 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; // console.log(prop, value,this,this._super,'===') // debugger; //在這裏調用父類的函數 this._super = _super; this._superApply = _superApply; returnValue = value.apply(this, arguments); this._super = __super; this._superApply = __superApply; // console.log(this,value,returnValue,prop,'===') return returnValue; }; })(); }); // console.info(proxiedPrototype) // debugger; //這裏是實例化獲取的內容 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: existingConstructor ?

(basePrototype.widgetEventPrefix || name) : name }, proxiedPrototype, { //又一次把constructor指向 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); } //將此方法掛在jQuery對象上 $.widget.bridge(name, constructor); return constructor; }; //擴展jq的extend方法,實際上類似$.extend(true,..) 深複製 $.widget.extend = function(target) { var input = widget_slice.call(arguments, 1), inputIndex = 0, inputLength = input.length, key, value; for (; inputIndex < inputLength; inputIndex++) { for (key in input[inputIndex]) { value = input[inputIndex][key]; if (input[inputIndex].hasOwnProperty(key) && value !== undefined) { // Clone objects if ($.isPlainObject(value)) { target[key] = $.isPlainObject(target[key]) ? $.widget.extend({}, target[key], value) : // Don't extend strings, arrays, etc. with objects $.widget.extend({}, value); // Copy everything else by reference } else { target[key] = value; } } } } return target; }; //bridge 是設計模式的一種,這裏將對象轉爲插件調用 $.widget.bridge = function(name, object) { var fullName = object.prototype.widgetFullName || name; //這裏就是插件了 //這部分的實現主要作了幾個工做,也是製做一個優雅的插件的主要代碼 //一、初次實例化時將插件對象緩存在dom上,興許則可直接調用,避免在一樣元素下widget的多實例化。

簡單的說,就是一個單例方法。 //二、合併用戶提供的默認設置選項options //三、可以經過調用插件時傳遞字符串來調用插件內的方法。

如:$('#id').menu('hide') 實際就是實例插件並調用hide()方法。

//四、同一時候限制外部調用「_」下劃線的私有方法 $.fn[name] = function(options) { var isMethodCall = typeof options === "string", args = widget_slice.call(arguments, 1), returnValue = this; // allow multiple hashes to be passed on init. //可以簡單以爲是$.extend(true,options,args[0],...),args可以是一個參數或是數組 options = !isMethodCall && args.length ? $.widget.extend.apply(null, [options].concat(args)) : options; //這裏對字符串和對象分別做處理 if (isMethodCall) { this.each(function() { var methodValue, instance = $.data(this, fullName); //假設傳遞的是instance則將this返回。 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"); } //這裏是假設傳遞的是字符串,則調用字符串方法,並傳遞相應的參數. //比方插件有個方法hide(a,b); 有2個參數:a。b //則調用時$('#id').menu('hide',1,2);//1和2 分別就是參數a和b了。 methodValue = instance[options].apply(instance, args); if (methodValue !== instance && methodValue !== undefined) { returnValue = methodValue && methodValue.jquery ?

returnValue.pushStack(methodValue.get()) : methodValue; return false; } }); } else { this.each(function() { var instance = $.data(this, fullName); if (instance) { instance.option(options || {}); //這裏每次都調用init方法 if (instance._init) { instance._init(); } } else { //緩存插件實例 $.data(this, fullName, new object(options, this)); } }); } return returnValue; }; }; //這裏是真正的widget基類 $.Widget = function( /* options, element */ ) {}; $.Widget._childConstructors = []; $.Widget.prototype = { widgetName: "widget", //用來決定事件的名稱和插件提供的callbacks的關聯。 // 比方dialog有一個close的callback,當close的callback被運行的時候,一個dialogclose的事件被觸發。 // 事件的名稱和事件的prefix+callback的名稱。widgetEventPrefix 默認就是控件的名稱。但是假設事件需要不一樣的名稱也可以被重寫。 // 比方一個用戶開始拖拽一個元素,咱們不想使用draggablestart做爲事件的名稱,咱們想使用dragstart,因此咱們可以重寫事件的prefix。 // 假設callback的名稱和事件的prefix一樣,事件的名稱將不會是prefix。

// 它阻止像dragdrag同樣的事件名稱。

widgetEventPrefix: "", defaultElement: "<div>", //屬性會在建立模塊時被覆蓋 options: { disabled: false, // callbacks create: null }, _createWidget: function(options, element) { element = $(element || this.defaultElement || this)[0]; this.element = $(element); this.uuid = widget_uuid++; this.eventNamespace = "." + this.widgetName + this.uuid; this.options = $.widget.extend({}, this.options, this._getCreateOptions(), options); this.bindings = $(); this.hoverable = $(); this.focusable = $(); if (element !== this) { // debugger $.data(element, this.widgetFullName, this); 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._create(); //建立插件時,有個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: function(key, value) { var options = key, parts, curOption, i; 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 }); }, _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 { // accept selectors, DOM elements 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.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++; } var match = event.match(/^([\w:-]*)\s*(.*)$/), eventName = match[1] + instance.eventNamespace, selector = match[2]; if (selector) { 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); }, _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"); } }); }, _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()); } }; $.each({ show: "fadeIn", hide: "fadeOut" }, function(method, defaultEffect) { $.Widget.prototype["_" + method] = function(element, options, callback) { if (typeof options === "string") { options = { effect: options }; } var hasOptions, effectName = !options ? method : options === true || typeof options === "number" ?

defaultEffect : options.effect || defaultEffect; options = options || {}; if (typeof options === "number") { options = { duration: options }; } hasOptions = !$.isEmptyObject(options); options.complete = callback; if (options.delay) { element.delay(options.delay); } if (hasOptions && $.effects && $.effects.effect[effectName]) { element[method](options); } else if (effectName !== method && element[effectName]) { element[effectName](options.duration, options.easing, callback); } else { element.queue(function(next) { $(this)[method](); if (callback) { callback.call(element[0]); } next(); }); } }; }); return $.widget; }));

相關文章
相關標籤/搜索