jquery ui 的所有组件都是基于一个简单,可重用的widget。
这个widget是jquery ui的核心部分,实用它能实现一致的API,创建有状态的插件,而无需关心插件的内部转换。
$.widget( name, base, prototype )
widget一共有2或3个参数。base为可选。
这里之所以把base放在第二个参数里,主要是因为这样写代码更直观一些。(因为后面的prototype 是个代码非常长的大对象)。
name:第一个参数是一个包含一个命名空间和组件名称的字符串,通过”.”来分割。
命名空间必须有,它指向widget prototype存储的全局jQuery对象。
如果命名空间没有,widget factory将会为你生成。widget name是插件函数和原型的真实名称,
比如: jQuery.widget( “demo.multi”, {…} ) 将会生成 jQuery.demo , jQuery.demo.multi , and jQuery.demo.multi.prototype .
base:第二个参数(可选)是 widget prototype继承于什么对象。
例如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的实例。
1 /*! 2 * jQuery UI Widget @VERSION 3 * http://jqueryui.com 4 * 5 * Copyright 2014 jQuery Foundation and other contributors 6 * Released under the MIT license. 7 * http://jquery.org/license 8 * 9 * http://api.jqueryui.com/jQuery.widget/ 10 */ 11 12 //这里判定是否支持amd or cmd 模式 13 (function(factory) { 14 if (typeof define === "function" && define.amd) { 15 16 // AMD. Register as an anonymous module. 17 define(["jquery"], factory); 18 } else { 19 20 // Browser globals 21 factory(jQuery); 22 } 23 }(function($) { 24 25 var widget_uuid = 0, 26 //插件的实例化数量 27 widget_slice = Array.prototype.slice; //数组的slice方法,这里的作用是将参赛arguments 转为真正的数组 28 29 //清除插件的数据及缓存 30 $.cleanData = (function(orig) { 31 return function(elems) { 32 for (var i = 0, elem; 33 (elem = elems[i]) != null; i++) { 34 try { 35 // 重写cleanData方法,调用后触发每个元素的remove事件 36 $(elem).triggerHandler("remove"); 37 // http://bugs.jquery.com/ticket/8235 38 } catch (e) {} 39 } 40 orig(elems); 41 }; 42 })($.cleanData); 43 44 /** 45 * widget工厂方法,用于创建插件 46 * @param name 包含命名空间的插件名称,格式 xx.xxx 47 * @param base 需要继承的ui组件 48 * @param prototype 插件的实际代码 49 * @returns {Function} 50 */ 51 $.widget = function(name, base, prototype) { 52 var fullName, //插件全称 53 existingConstructor, //原有的构造函数 54 constructor, //当前构造函数 55 basePrototype, //父类的Prototype 56 // proxiedPrototype allows the provided prototype to remain unmodified 57 // so that it can be used as a mixin for multiple widgets (#8876) 58 proxiedPrototype = {}, 59 //可调用父类方法_spuer的prototype对象,扩展于prototype 60 namespace = name.split(".")[0]; 61 62 name = name.split(".")[1]; 63 fullName = namespace + "-" + name; 64 //如果只有2个参数 base默认为Widget类,组件默认会继承base类的所有方法 65 if (!prototype) { 66 prototype = base; 67 base = $.Widget; 68 } 69 70 // console.log(base, $.Widget) 71 72 // create selector for plugin 73 //创建一个自定义的伪类选择器 74 //如 $(':ui-menu') 则表示选择定义了ui-menu插件的元素 75 $.expr[":"][fullName.toLowerCase()] = function(elem) { 76 return !!$.data(elem, fullName); 77 }; 78 79 // 判定命名空间对象是否存在,没有的话 则创建一个空对象 80 $[namespace] = $[namespace] || {}; 81 //这里存一份旧版的插件,如果这个插件已经被使用或者定义了 82 existingConstructor = $[namespace][name]; 83 //这个是插件实例化的主要部分 84 //constructor存储了插件的实例,同时也创建了基于命名空间的对象 85 //如$.ui.menu 86 constructor = $[namespace][name] = function(options, element) { 87 // allow instantiation without "new" keyword 88 //允许直接调用命名空间上的方法来创建组件 89 //比如:$.ui.menu({},'#id') 这种方式创建的话,默认没有new 实例化。因为_createWidget是prototype上的方法,需要new关键字来实例化 90 //通过 调用 $.ui.menu 来实例化插件 91 if (!this._createWidget) { 92 console.info(this) 93 return new constructor(options, element); 94 } 95 96 // allow instantiation without initializing for simple inheritance 97 // must use "new" keyword (the code above always passes args) 98 //如果存在参数,则说明是正常调用插件 99 //_createWidget是创建插件的核心方法 100 if (arguments.length) { 101 this._createWidget(options, element); 102 } 103 }; 104 // extend with the existing constructor to carry over any static properties 105 //合并对象,将旧插件实例,及版本号、prototype合并到constructor 106 $.extend(constructor, existingConstructor, { 107 108 version: prototype.version, 109 // copy the object used to create the prototype in case we need to 110 // redefine the widget later 111 //创建一个新的插件对象 112 //将插件实例暴露给外部,可用户修改及覆盖 113 _proto: $.extend({}, prototype), 114 // track widgets that inherit from this widget in case this widget is 115 // redefined after a widget inherits from it 116 _childConstructors: [] 117 }); 118 119 //实例化父类 获取父类的 prototype 120 basePrototype = new base(); 121 // we need to make the options hash a property directly on the new instance 122 // otherwise we'll modify the options hash on the prototype that we're 123 // inheriting from 124 //这里深复制一份options 125 basePrototype.options = $.widget.extend({}, basePrototype.options); 126 //在传入的ui原型中有方法调用this._super 和this.__superApply会调用到base上(最基类上)的方法 127 $.each(prototype, function(prop, value) { 128 //如果val不是function 则直接给对象赋值字符串 129 if (!$.isFunction(value)) { 130 proxiedPrototype[prop] = value; 131 return; 132 } 133 //如果val是function 134 proxiedPrototype[prop] = (function() { 135 //两种调用父类函数的方法 136 var _super = function() { 137 //将当期实例调用父类的方法 138 return base.prototype[prop].apply(this, arguments); 139 }, 140 _superApply = function(args) { 141 return base.prototype[prop].apply(this, args); 142 }; 143 return function() { 144 var __super = this._super, 145 __superApply = this._superApply, 146 returnValue; 147 // console.log(prop, value,this,this._super,'===') 148 // debugger; 149 //在这里调用父类的函数 150 this._super = _super; 151 this._superApply = _superApply; 152 153 returnValue = value.apply(this, arguments); 154 155 this._super = __super; 156 this._superApply = __superApply; 157 // console.log(this,value,returnValue,prop,'===') 158 return returnValue; 159 }; 160 })(); 161 }); 162 // console.info(proxiedPrototype) 163 // debugger; 164 //这里是实例化获取的内容 165 constructor.prototype = $.widget.extend(basePrototype, { 166 // TODO: remove support for widgetEventPrefix 167 // always use the name + a colon as the prefix, e.g., draggable:start 168 // don't prefix for widgets that aren't DOM-based 169 widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name 170 }, proxiedPrototype, { 171 //重新把constructor指向 constructor 变量 172 constructor: constructor, 173 namespace: namespace, 174 widgetName: name, 175 widgetFullName: fullName 176 }); 177 178 // If this widget is being redefined then we need to find all widgets that 179 // are inheriting from it and redefine all of them so that they inherit from 180 // the new version of this widget. We're essentially trying to replace one 181 // level in the prototype chain. 182 //这里判定插件是否被使用了。一般来说,都不会被使用的。 183 //因为插件的开发者都是我们自己,呵呵 184 if (existingConstructor) { 185 $.each(existingConstructor._childConstructors, function(i, child) { 186 var childPrototype = child.prototype; 187 188 // redefine the child widget using the same prototype that was 189 // originally used, but inherit from the new version of the base 190 $.widget(childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto); 191 }); 192 // remove the list of existing child constructors from the old constructor 193 // so the old child constructors can be garbage collected 194 delete existingConstructor._childConstructors; 195 } else { 196 //父类添加当前插件的实例 主要用于作用域链查找 不至于断层 197 base._childConstructors.push(constructor); 198 } 199 200 //将此方法挂在jQuery对象上 201 $.widget.bridge(name, constructor); 202 203 return constructor; 204 }; 205 206 //扩展jq的extend方法,实际上类似$.extend(true,..) 深复制 207 $.widget.extend = function(target) { 208 var input = widget_slice.call(arguments, 1), 209 inputIndex = 0, 210 inputLength = input.length, 211 key, value; 212 for (; inputIndex < inputLength; inputIndex++) { 213 for (key in input[inputIndex]) { 214 value = input[inputIndex][key]; 215 if (input[inputIndex].hasOwnProperty(key) && value !== undefined) { 216 // Clone objects 217 if ($.isPlainObject(value)) { 218 target[key] = $.isPlainObject(target[key]) ? $.widget.extend({}, target[key], value) : 219 // Don't extend strings, arrays, etc. with objects 220 $.widget.extend({}, value); 221 // Copy everything else by reference 222 } else { 223 target[key] = value; 224 } 225 } 226 } 227 } 228 return target; 229 }; 230 231 //bridge 是设计模式的一种,这里将对象转为插件调用 232 $.widget.bridge = function(name, object) { 233 var fullName = object.prototype.widgetFullName || name; 234 //这里就是插件了 235 //这部分的实现主要做了几个工作,也是制作一个优雅的插件的主要代码 236 //1、初次实例化时将插件对象缓存在dom上,后续则可直接调用,避免在相同元素下widget的多实例化。简单的说,就是一个单例方法。 237 //2、合并用户提供的默认设置选项options 238 //3、可以通过调用插件时传递字符串来调用插件内的方法。如:$('#id').menu('hide') 实际就是实例插件并调用hide()方法。 239 //4、同时限制外部调用“_”下划线的私有方法 240 $.fn[name] = function(options) { 241 var isMethodCall = typeof options === "string", 242 args = widget_slice.call(arguments, 1), 243 returnValue = this; 244 245 // allow multiple hashes to be passed on init. 246 //可以简单认为是$.extend(true,options,args[0],...),args可以是一个参数或是数组 247 options = !isMethodCall && args.length ? $.widget.extend.apply(null, [options].concat(args)) : options; 248 //这里对字符串和对象分别作处理 249 if (isMethodCall) { 250 this.each(function() { 251 var methodValue, instance = $.data(this, fullName); 252 //如果传递的是instance则将this返回。 253 if (options === "instance") { 254 returnValue = instance; 255 return false; 256 } 257 if (!instance) { 258 return $.error("cannot call methods on " + name + " prior to initialization; " + "attempted to call method '" + options + "'"); 259 } 260 //这里对私有方法的调用做了限制,直接调用会抛出异常事件 261 if (!$.isFunction(instance[options]) || options.charAt(0) === "_") { 262 return $.error("no such method '" + options + "' for " + name + " widget instance"); 263 } 264 //这里是如果传递的是字符串,则调用字符串方法,并传递对应的参数. 265 //比如插件有个方法hide(a,b); 有2个参数:a,b 266 //则调用时$('#id').menu('hide',1,2);//1和2 分别就是参数a和b了。 267 methodValue = instance[options].apply(instance, args); 268 if (methodValue !== instance && methodValue !== undefined) { 269 returnValue = methodValue && methodValue.jquery ? returnValue.pushStack(methodValue.get()) : methodValue; 270 return false; 271 } 272 }); 273 } else { 274 this.each(function() { 275 var instance = $.data(this, fullName); 276 277 if (instance) { 278 instance.option(options || {}); 279 //这里每次都调用init方法 280 if (instance._init) { 281 instance._init(); 282 } 283 } else { 284 //缓存插件实例 285 $.data(this, fullName, new object(options, this)); 286 } 287 }); 288 } 289 290 return returnValue; 291 }; 292 }; 293 294 //这里是真正的widget基类 295 $.Widget = function( /* options, element */ ) {}; 296 $.Widget._childConstructors = []; 297 298 $.Widget.prototype = { 299 widgetName: "widget", 300 //用来决定事件的名称和插件提供的callbacks的关联。 301 // 比如dialog有一个close的callback,当close的callback被执行的时候,一个dialogclose的事件被触发。 302 // 事件的名称和事件的prefix+callback的名称。widgetEventPrefix 默认就是控件的名称,但是如果事件需要不同的名称也可以被重写。 303 // 比如一个用户开始拖拽一个元素,我们不想使用draggablestart作为事件的名称,我们想使用dragstart,所以我们可以重写事件的prefix。 304 // 如果callback的名称和事件的prefix相同,事件的名称将不会是prefix。 305 // 它阻止像dragdrag一样的事件名称。 306 widgetEventPrefix: "", 307 defaultElement: "<div>", 308 //属性会在创建模块时被覆盖 309 options: { 310 disabled: false, 311 312 // callbacks 313 create: null 314 }, 315 _createWidget: function(options, element) { 316 element = $(element || this.defaultElement || this)[0]; 317 this.element = $(element); 318 this.uuid = widget_uuid++; 319 this.eventNamespace = "." + this.widgetName + this.uuid; 320 this.options = $.widget.extend({}, this.options, this._getCreateOptions(), options); 321 322 this.bindings = $(); 323 this.hoverable = $(); 324 this.focusable = $(); 325 326 if (element !== this) { 327 // debugger 328 $.data(element, this.widgetFullName, this); 329 this._on(true, this.element, { 330 remove: function(event) { 331 if (event.target === element) { 332 this.destroy(); 333 } 334 } 335 }); 336 this.document = $(element.style ? 337 // element within the document 338 element.ownerDocument : 339 // element is window or document 340 element.document || element); 341 this.window = $(this.document[0].defaultView || this.document[0].parentWindow); 342 } 343 344 this._create(); 345 //创建插件时,有个create的回调 346 this._trigger("create", null, this._getCreateEventData()); 347 this._init(); 348 }, 349 _getCreateOptions: $.noop, 350 _getCreateEventData: $.noop, 351 _create: $.noop, 352 _init: $.noop, 353 //销毁模块:去除绑定事件、去除数据、去除样式、属性 354 destroy: function() { 355 this._destroy(); 356 // we can probably remove the unbind calls in 2.0 357 // all event bindings should go through this._on() 358 this.element.unbind(this.eventNamespace).removeData(this.widgetFullName) 359 // support: jquery <1.6.3 360 // http://bugs.jquery.com/ticket/9413 361 .removeData($.camelCase(this.widgetFullName)); 362 this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass( 363 this.widgetFullName + "-disabled " + "ui-state-disabled"); 364 365 // clean up events and states 366 this.bindings.unbind(this.eventNamespace); 367 this.hoverable.removeClass("ui-state-hover"); 368 this.focusable.removeClass("ui-state-focus"); 369 }, 370 _destroy: $.noop, 371 372 widget: function() { 373 return this.element; 374 }, 375 //设置选项函数 376 option: function(key, value) { 377 var options = key, 378 parts, curOption, i; 379 380 if (arguments.length === 0) { 381 // don't return a reference to the internal hash 382 //返回一个新的对象,不是内部数据的引用 383 return $.widget.extend({}, this.options); 384 } 385 386 if (typeof key === "string") { 387 // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } 388 options = {}; 389 parts = key.split("."); 390 key = parts.shift(); 391 if (parts.length) { 392 curOption = options[key] = $.widget.extend({}, this.options[key]); 393 for (i = 0; i < parts.length - 1; i++) { 394 curOption[parts[i]] = curOption[parts[i]] || {}; 395 curOption = curOption[parts[i]]; 396 } 397 key = parts.pop(); 398 if (arguments.length === 1) { 399 return curOption[key] === undefined ? null : curOption[key]; 400 } 401 curOption[key] = value; 402 } else { 403 if (arguments.length === 1) { 404 return this.options[key] === undefined ? null : this.options[key]; 405 } 406 options[key] = value; 407 } 408 } 409 410 this._setOptions(options); 411 412 return this; 413 }, 414 _setOptions: function(options) { 415 var key; 416 417 for (key in options) { 418 this._setOption(key, options[key]); 419 } 420 421 return this; 422 }, 423 _setOption: function(key, value) { 424 this.options[key] = value; 425 426 if (key === "disabled") { 427 this.widget().toggleClass(this.widgetFullName + "-disabled", !! value); 428 429 // If the widget is becoming disabled, then nothing is interactive 430 if (value) { 431 this.hoverable.removeClass("ui-state-hover"); 432 this.focusable.removeClass("ui-state-focus"); 433 } 434 } 435 436 return this; 437 }, 438 439 enable: function() { 440 return this._setOptions({ 441 disabled: false 442 }); 443 }, 444 disable: function() { 445 return this._setOptions({ 446 disabled: true 447 }); 448 }, 449 450 _on: function(suppressDisabledCheck, element, handlers) { 451 var delegateElement, instance = this; 452 453 // no suppressDisabledCheck flag, shuffle arguments 454 if (typeof suppressDisabledCheck !== "boolean") { 455 handlers = element; 456 element = suppressDisabledCheck; 457 suppressDisabledCheck = false; 458 } 459 460 // no element argument, shuffle and use this.element 461 if (!handlers) { 462 handlers = element; 463 element = this.element; 464 delegateElement = this.widget(); 465 } else { 466 // accept selectors, DOM elements 467 element = delegateElement = $(element); 468 this.bindings = this.bindings.add(element); 469 } 470 471 $.each(handlers, function(event, handler) { 472 function handlerProxy() { 473 // allow widgets to customize the disabled handling 474 // - disabled as an array instead of boolean 475 // - disabled class as method for disabling individual parts 476 if (!suppressDisabledCheck && (instance.options.disabled === true || $(this).hasClass("ui-state-disabled"))) { 477 return; 478 } 479 return (typeof handler === "string" ? instance[handler] : handler).apply(instance, arguments); 480 } 481 482 // copy the guid so direct unbinding works 483 if (typeof handler !== "string") { 484 handlerProxy.guid = handler.guid = handler.guid || handlerProxy.guid || $.guid++; 485 } 486 487 var match = event.match(/^([\w:-]*)\s*(.*)$/), 488 eventName = match[1] + instance.eventNamespace, 489 selector = match[2]; 490 if (selector) { 491 delegateElement.delegate(selector, eventName, handlerProxy); 492 } else { 493 element.bind(eventName, handlerProxy); 494 } 495 }); 496 }, 497 498 _off: function(element, eventName) { 499 eventName = (eventName || "").split(" ").join(this.eventNamespace + " ") + this.eventNamespace; 500 element.unbind(eventName).undelegate(eventName); 501 }, 502 503 _delay: function(handler, delay) { 504 function handlerProxy() { 505 return (typeof handler === "string" ? instance[handler] : handler).apply(instance, arguments); 506 } 507 var instance = this; 508 return setTimeout(handlerProxy, delay || 0); 509 }, 510 511 _hoverable: function(element) { 512 this.hoverable = this.hoverable.add(element); 513 this._on(element, { 514 mouseenter: function(event) { 515 $(event.currentTarget).addClass("ui-state-hover"); 516 }, 517 mouseleave: function(event) { 518 $(event.currentTarget).removeClass("ui-state-hover"); 519 } 520 }); 521 }, 522 523 _focusable: function(element) { 524 this.focusable = this.focusable.add(element); 525 this._on(element, { 526 focusin: function(event) { 527 $(event.currentTarget).addClass("ui-state-focus"); 528 }, 529 focusout: function(event) { 530 $(event.currentTarget).removeClass("ui-state-focus"); 531 } 532 }); 533 }, 534 535 _trigger: function(type, event, data) { 536 var prop, orig, callback = this.options[type]; 537 538 data = data || {}; 539 event = $.Event(event); 540 event.type = (type === this.widgetEventPrefix ? type : this.widgetEventPrefix + type).toLowerCase(); 541 // the original event may come from any element 542 // so we need to reset the target on the new event 543 event.target = this.element[0]; 544 545 // copy original event properties over to the new event 546 orig = event.originalEvent; 547 if (orig) { 548 for (prop in orig) { 549 if (!(prop in event)) { 550 event[prop] = orig[prop]; 551 } 552 } 553 } 554 555 this.element.trigger(event, data); 556 return !($.isFunction(callback) && callback.apply(this.element[0], [event].concat(data)) === false || event.isDefaultPrevented()); 557 } 558 }; 559 560 $.each({ 561 show: "fadeIn", 562 hide: "fadeOut" 563 }, function(method, defaultEffect) { 564 $.Widget.prototype["_" + method] = function(element, options, callback) { 565 if (typeof options === "string") { 566 options = { 567 effect: options 568 }; 569 } 570 var hasOptions, effectName = !options ? method : options === true || typeof options === "number" ? defaultEffect : options.effect || defaultEffect; 571 options = options || {}; 572 if (typeof options === "number") { 573 options = { 574 duration: options 575 }; 576 } 577 hasOptions = !$.isEmptyObject(options); 578 options.complete = callback; 579 if (options.delay) { 580 element.delay(options.delay); 581 } 582 if (hasOptions && $.effects && $.effects.effect[effectName]) { 583 element[method](options); 584 } else if (effectName !== method && element[effectName]) { 585 element[effectName](options.duration, options.easing, callback); 586 } else { 587 element.queue(function(next) { 588 $(this)[method](); 589 if (callback) { 590 callback.call(element[0]); 591 } 592 next(); 593 }); 594 } 595 }; 596 }); 597 598 return $.widget; 599 600 }));</div>