zoukankan      html  css  js  c++  java
  • widget jquery 理解

    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>
  • 相关阅读:
    hdu5728 PowMod
    CF1156E Special Segments of Permutation
    CF1182E Product Oriented Recurrence
    CF1082E Increasing Frequency
    CF623B Array GCD
    CF1168B Good Triple
    CF1175E Minimal Segment Cover
    php 正则
    windows 下安装composer
    windows apache "The requested operation has failed" 启动失败
  • 原文地址:https://www.cnblogs.com/wang985850293/p/5287515.html
Copyright © 2011-2022 走看看