jQuery技术

【代码片-1】 jQuery源码学习(版本1.11)-事件处理-工具函数jQuery.event

字号+ 作者:H5之家 来源:H5之家 2017-06-22 15:01 我要评论( )

CSDN知识库,按照技术领域和知识结构对海量技术资源进行筛选整理,并通过相关机制进行审核收录。保证入库知识的系统性与优质性。并且所含技术知识图谱有清晰的学

// 事件管理的工具函数 jQuery.event = { global: {}, // 添加绑定事件 add: function( elem, types, handler, data, selector ) { var tmp, events, t, handleObjIn, special, eventHandle, handleObj, handlers, type, namespaces, origType, // 获取elem在jQuery.cache中对应的缓存(如果没定义_data会自动初始化一个新的) elemData = jQuery._data( elem ); // elemData即缓存不存在,说明elem不适合绑定事件,直接返回 if ( !elemData ) { return; } // 处理handler是一个object而不单纯是funciton的场景 if ( handler.handler ) { handleObjIn = handler; handler = handleObjIn.handler; selector = handleObjIn.selector; } // 为handler增加一个独有id,用于remove判断解除绑定的是不是同一个回调(我估计是直接用===比较引用会损耗性能) if ( !handler.guid ) { handler.guid = jQuery.guid++; } // 初始化缓存里的events对象,后面用于保存事件类型和对应回调 if ( !(events = elemData.events) ) { events = elemData.events = {}; } // 初始化elemData.handle,统一执行所有回调的函数 if ( !(eventHandle = elemData.handle) ) { eventHandle = elemData.handle = function( e ) { // 分发处理事件,trigger方法中会使用原生监听方法将eventHandle作为原生方法的回调 return typeof jQuery !== strundefined && (!e || jQuery.event.triggered !== e.type) ? jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : undefined; }; // 防止IE内存泄露 eventHandle.elem = elem; } // 本方法支持事件类型用空格隔开传入,处理type保存了多种事件类型的场景,将其分割开 types = ( types || "" ).match( rnotwhite ) || [ "" ]; t = types.length; while ( t-- ) { // rtypenamespace这个正则是用来匹配"."前后片段的,最终过滤得到type是需要的事件类型 // 具体考虑场景暂时还不确定,估计是用来兼容一些传入了命名空间的场景 // 命名空间是提供一种根据不同命名空间来处理事件的机制 tmp = rtypenamespace.exec( types[t] ) || []; // 由代码可推测tmp[1]是原始类型 type = origType = tmp[1]; // tmp[2]是命名空间,使用split分割成数组 namespaces = ( tmp[2] || "" ).split( "." ).sort(); //最终得到需要的type,不存在的话就循环下一个 if ( !type ) { continue; } // 获取其special里保存的事件类型对应的对象 // 后面代码可知,load,focus,blur,click,beforeunload五种事件类型均有对应的特殊对象 // 不存在则使用空对象 special = jQuery.event.special[ type ] || {}; // 如果selector存在,则使用special.delegateType作为type,否则使用special.bindType // special.delegateType和special.bindType都为空(前面special可能只是空对象),则不改变type type = ( selector ? special.delegateType : special.bindType ) || type; // 上一行代码type可能改变了,因此更新一下对应的special对象 special = jQuery.event.special[ type ] || {}; // 构造一个handleObj // 这里使用extend方法将handleObjIn扩展进handleObj,假如handleObjIn为空,就会直接返回handleObj // extend内部确定需要扩展的target时,使用的是形参长度,因此只要用来扩展的对象传了形参,即使为空,也不会变为将handleObj扩展进jQuery // 只当传了一个参数时,target才会变为this,即jQuery本身 handleObj = jQuery.extend({ type: type, origType: origType, data: data, handler: handler, guid: handler.guid, selector: selector, needsContext: selector && jQuery.expr.match.needsContext.test( selector ), // 保存命名空间,网上查找用法说是可让用户根据命名空间,对事件进行不同的处理 namespace: namespaces.join(".") }, handleObjIn ); // 初始化该事件类型的缓存 if ( !(handlers = events[ type ]) ) { // 初始化缓存数组handlers,用于保存回调 handlers = events[ type ] = []; handlers.delegateCount = 0; // setup暂时不理解是什么东西 if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { // 调用elem.addEventListener或elem.attachEvent监听事件,回调使用eventHandle // eventHandle里面调用jQuery.event.dispatch,会对缓存里面的对应事件类型回调一一执行 if ( elem.addEventListener ) { elem.addEventListener( type, eventHandle, false ); } else if ( elem.attachEvent ) { elem.attachEvent( "on" + type, eventHandle ); } } } if ( special.add ) { special.add.call( elem, handleObj ); // 感觉这是多余的分支,从前面代码分析,handleObj.handler跟handler永远是相同的引用 if ( !handleObj.handler.guid ) { handleObj.handler.guid = handler.guid; } } // 将回调handleObj添加进handlers if ( selector ) { // 这里使用splice时插入对象(handlers.delegateCount是位置,第二个参数是0表明不删除,第三个是需要插入数组的对象,会被放在handlers.delegateCount前面) // 由于是插入的,因此事件分发执行的时候也会被优先执行 handlers.splice( handlers.delegateCount++, 0, handleObj ); } else { handlers.push( handleObj ); } // 记录曾经使用过的事件类型 jQuery.event.global[ type ] = true; } // 规避ie的内存泄露问题 elem = null; }, // 解除绑定事件 remove: function( elem, types, handler, selector, mappedTypes ) { var j, handleObj, tmp, origCount, t, events, special, handlers, type, namespaces, origType, // 判断后获取elem缓存 elemData = jQuery.hasData( elem ) && jQuery._data( elem ); // elemData不存在或elemData.events则返回,这里同时也声明了引用events = elemData.events if ( !elemData || !(events = elemData.events) ) { return; } // 兼容空格分开多种事件类型的场景 types = ( types || "" ).match( rnotwhite ) || [ "" ]; t = types.length; // 循环遍历需要移除的事件类型 while ( t-- ) { tmp = rtypenamespace.exec( types[t] ) || []; // 获取原始类型,让origType引用上,后续会用到origType type = origType = tmp[1]; namespaces = ( tmp[2] || "" ).split( "." ).sort(); // 如果type为空,说明types传入的也是一个空值,则需要解除绑定所有的事件监听 if ( !type ) { for ( type in events ) { jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); } continue; } special = jQuery.event.special[ type ] || {}; type = ( selector ? special.delegateType : special.bindType ) || type; // 获取事件类型对应的保存回调的数组 handlers = events[ type ] || []; // 下面一行暂时不明白意义何在 tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); // Remove matching events origCount = j = handlers.length; // 循环遍历,找到需要移除的回调 while ( j-- ) { handleObj = handlers[ j ]; // 要同时满足多个表达式,才进入分支移除回调 if ( ( mappedTypes || origType === handleObj.origType ) && ( !handler || handler.guid === handleObj.guid ) && ( !tmp || tmp.test( handleObj.namespace ) ) && ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { // 调用splice移除对应的回调 handlers.splice( j, 1 ); if ( handleObj.selector ) { handlers.delegateCount--; } if ( special.remove ) { special.remove.call( elem, handleObj ); } } } // Remove generic event handler if we removed something and no more handlers exist // (avoids potential for endless recursion during removal of special event handlers) if ( origCount && !handlers.length ) { if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { jQuery.removeEvent( elem, type, elemData.handle ); } delete events[ type ]; } } // Remove the expando if it's no longer used if ( jQuery.isEmptyObject( events ) ) { delete elemData.handle; // removeData also checks for emptiness and clears the expando if empty // so use it instead of delete jQuery._removeData( elem, "events" ); } }, // 主动触发事件函数 trigger: function( event, data, elem, onlyHandlers ) { var handle, ontype, cur, bubbleType, special, tmp, i, // 定义需要触发事件的元素队列,后续用于保存父节点,制造冒泡效果,让父节点也触发事件 eventPath = [ elem || document ], // 确定事件类型 type = hasOwn.call( event, "type" ) ? event.type : event, // 确定命名空间 namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; // 定义当前cur,tmp,确定当前触发事件的元素 cur = tmp = elem = elem || document; // 不处理text跟comment节点的场景 if ( elem.nodeType === 3 || elem.nodeType === 8 ) { return; } // 不触发focus/blur focusin/out事件? if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { return; } // 校验type是否有命名空间 if ( type.indexOf(".") >= 0 ) { // 分割type namespaces = type.split("."); // 获取第一个作为原始类型 type = namespaces.shift(); // 其他的全部作为事件类型的命名空间,命名空间的作用是可让使用者根据不同的情况处理事件 namespaces.sort(); } // 获取原生事件类型,例如onclick,后面用于查找elem内是否定义了原生事件的回调 ontype = type.indexOf(":") < 0 && "on" + type; // 如果event不存在jQuery.expando属性,则使用jQuery.Event重新创建一个 event = event[ jQuery.expando ] ? event : new jQuery.Event( type, typeof event === "object" && event ); // 以下几行增加了event的一些属性 // 根据onlyHandlers确定event.isTrigger,值是2或3,用处暂时不明 event.isTrigger = onlyHandlers ? 2 : 3; event.namespace = namespaces.join("."); event.namespace_re = event.namespace ? new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : null; // 清除一下event.result,因为有可能之前触发过 event.result = undefined; // 确定event.target if ( !event.target ) { event.target = elem; } // 将data数据克隆进新数组中返回(前提是data是类数组对象,否则makeArray会直接将data加进新数组,这样就是引用了原data对象而不是克隆) data = data == null ? [ event ] : jQuery.makeArray( data, [ event ] ); // 事件类型的特殊处理方法,如果满足条件,也需要触发 special = jQuery.event.special[ type ] || {}; if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { return; } // onlyHandlers的应该是决定是否触发事件的默认行为,如冒泡 // onlyHandlers变量字面意思,true的时候只触发该事件保存的handlers,而不触发默认行为,为false的时候才能够触发默认行为 if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { bubbleType = special.delegateType || type; if ( !rfocusMorph.test( bubbleType + type ) ) { cur = cur.parentNode; } // 遍历当前事件节点的父节点 for ( ; cur; cur = cur.parentNode ) { // 塞进需要触发事件的元素队列 eventPath.push( cur ); // 重置tmp tmp = cur; } // 最后增加一个tmp.defaultView || tmp.parentWindow || window ,必须确定tmp是elem的ownerDocument或当前document这个变量的引用对象 if ( tmp === (elem.ownerDocument || document) ) { eventPath.push( tmp.defaultView || tmp.parentWindow || window ); } } // 冒泡事件,要满足event.isPropagationStopped()为false的条件(这里怀疑存在问题,原始节点的事件触发应该是不需要判断isPropagationStopped的) i = 0; while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { // 第一个是必须触发的原始事件,因此这里判断一下,当i > 1后,就是冒泡事件了 event.type = i > 1 ? bubbleType : special.bindType || type; // 从之前分析的代码可知,events是一个对象,属性是事件类型,值是保存了回调的数组 // 以下代码是先判断回调数组是否存在,然后再直接获取jQuery._data( cur, "handle" ),前面代码可知这是统一执行所有回调的函数 handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); // 执行事件回调 if ( handle ) { handle.apply( cur, data ); } // 从节点中获取原生事件回调 handle = ontype && cur[ ontype ]; // 执行原生事件回调 if ( handle && handle.apply && jQuery.acceptData( cur ) ) { // 定义事件执行结果 event.result = handle.apply( cur, data ); // 返回false的话要停止事件默认行为(这个是DOM标准吗?还是jquery自己增加的功能) if ( event.result === false ) { event.preventDefault(); } } } event.type = type; // 触发事件的默认行为 if ( !onlyHandlers && !event.isDefaultPrevented() ) { if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && jQuery.acceptData( elem ) ) { // 先判断ontype,elem[ type ]是否存在,而elem不能为window,才能触发默认行为 if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { // 引用原生事件回调 tmp = elem[ ontype ]; // 然后清空elem[ ontype ],防止触发默认行为的时候再度触发事件 if ( tmp ) { elem[ ontype ] = null; } // Prevent re-triggering of the same event, since we already bubbled it above jQuery.event.triggered = type; try { // 触发事件默认行为(原来去掉on之后在节点里的这个属性就是事件的默认行为函数,第一次了解到这个知识) elem[ type ](); } catch ( e ) { // 捕获IE<9的异常,以免影响脚本运行 } jQuery.event.triggered = undefined; // 触发完默认行为后再重置原生事件elem[ ontype ]引用 if ( tmp ) { elem[ ontype ] = tmp; } } } } // 返回执行结果 return event.result; }, // 事件分发 dispatch: function( event ) { // 调用fix将传入事件修复 event = jQuery.event.fix( event ); var i, ret, handleObj, matched, j, handlerQueue = [], args = slice.call( arguments ), // 从缓存中获取elem对应的回调数组(dispatch方法没有elem的形参,但是从前面的add方法里可以看出,使用apply去保证this指向elem) handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], // 获取对应事件类型的special对象 special = jQuery.event.special[ event.type ] || {}; // 改变入参指向,使用fix之后的event args[0] = event; event.delegateTarget = this; // 暂时不理解preDispatch的具体使用场景 if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { return; } // jQuery.event.handlers返回的是一个回调数组,里面保存的是对象,每个对象都存在属性elem,以及handlers(这个是数组,里面保存handleObj,与events[type]类似) handlerQueue = jQuery.event.handlers.call( this, event, handlers ); // 首先处理的是delegateCount的回调对象 i = 0; while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { event.currentTarget = matched.elem; j = 0; // 遍历matched.handlers,定义局部变量handleObj,后续使用 while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { // 触发事件的事件类型不能存在命名空间 if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { event.handleObj = handleObj; event.data = handleObj.data; // 执行回调,获取返回结果ret ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) .apply( matched.elem, args ); if ( ret !== undefined ) { // 返回结果为false,阻止事件默认行为,并且停止冒泡 if ( (event.result = ret) === false ) { event.preventDefault(); event.stopPropagation(); } } } } } // 执行postDispatch if ( special.postDispatch ) { special.postDispatch.call( this, event ); } // 返回事件结果,从前面的逻辑可以看出,每一个回调都可能会有result // 这样的话event.result实际取的就是最后一个有result的回调的result return event.result; }, // 生成一个数组handlerQueue,用于分发事件(入参两个,event是jQuery.Event对象,handlers是回调对象缓存数组events[type]) handlers: function( event, handlers ) { var sel, handleObj, matches, i, handlerQueue = [], // 获取delegateCount // 从前面代码分析,当缓存数组events[type]里的回调对象handlerObj,存在selector的时候,就会被插入到数组前面 // 因此这里获取到的delegateCount表明的应该是需要特殊处理的回调对象的个数 delegateCount = handlers.delegateCount, // 获取当前事件作用的节点 cur = event.target; // 满足条件的进入分支, if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { // 从cur开始,然后第二轮是遍历获取cur.parentNode(要进入这个分支,首先第一遍cur就不能为this) for ( ; cur != this; cur = cur.parentNode || this ) { // 必须是element节点 if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { matches = []; // 遍历delegateCount for ( i = 0; i < delegateCount; i++ ) { handleObj = handlers[ i ]; // 获取handleObj.selector,后面加空格是为了避免与属性冲突 sel = handleObj.selector + " "; // 下面分支使用Sizzle找到selector对应元素的个数,存进matches[ sel ] // 从这里可以猜测jQuery.event的delegateCount这个功能,应该是为了对特殊选择的某些元素执行回调而存在的 // 由于外面使用了双重循环,这样会导致matches.push( handleObj )多次执行,matches存在多个重复handleObj if ( matches[ sel ] === undefined ) { matches[ sel ] = handleObj.needsContext ? jQuery( sel, this ).index( cur ) >= 0 : jQuery.find( sel, this, null, [ cur ] ).length; } // 个数大于0,则把handleObj放进数组 if ( matches[ sel ] ) { matches.push( handleObj ); } } // 数组长度大于0,说明存放了handleObj,这些存放的handleObj的selector找到的元素个数不为0,塞进handlerQueue if ( matches.length ) { handlerQueue.push({ elem: cur, handlers: matches }); } } } } // 将剩余的handleObj截取出新数组,放进对象的handlers属性然后push进handlerQueue if ( delegateCount < handlers.length ) { handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); } // 返回数组 return handlerQueue; }, fix: function( event ) { // 使用JQ版本标志判断event是否jQuery.Event对象,是则直接返回 if ( event[ jQuery.expando ] ) { return event; } // Create a writable copy of the event object and normalize some properties var i, prop, copy, // 事件类型 type = event.type, // 后面代码event引用会改变为新事件对象,这里先引用旧事件对象 originalEvent = event, // 对应勾子对象 fixHook = this.fixHooks[ type ]; // 若对应事件类型的勾子不存在,则重新定义 // 判断其是否是特殊事件类型,是则使用其勾子,否则定义为空对象 if ( !fixHook ) { this.fixHooks[ type ] = fixHook = rmouseEvent.test( type ) ? this.mouseHooks : rkeyEvent.test( type ) ? this.keyHooks : {}; } // 引用勾子对象的属性props // this.props则是jQuery.event.props里的元素,使用concat构造新数组,表明这些也是构建事件对象需要的属性 copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; // 建立一个新事件对象 event = new jQuery.Event( originalEvent ); // 循环将属性拷贝进新事件对象 i = copy.length; while ( i-- ) { prop = copy[ i ]; event[ prop ] = originalEvent[ prop ]; } // 兼容IE<9,IE不存在target,但存在srcElement,为了后面方便使用,这里统一兼容成target if ( !event.target ) { event.target = originalEvent.srcElement || document; } // 兼容Chrome 23+, Safari存在的target为text类型的场景,改变为其父对象,不能使用text类型 if ( event.target.nodeType === 3 ) { event.target = event.target.parentNode; } // 兼容IE<9,metaKey会出现为undefined的场景,将其修正为boolean类型,undefined为变为false event.metaKey = !!event.metaKey; // 若勾子存在过滤器,则返回过滤后的事件对象,否则直接返回新事件对象 return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; }, // 包含一些事件常用属性,让KeyEvent和MouseEvent共享 // 新建JQ事件对象的时候,这些属性就会被扩展进去 props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), // fix的勾子,应该是提供自定义勾子使用的,如果不存在的话,fix方法里面会使用keyHooks跟mouseHooks fixHooks: {}, // 键盘事件勾子 keyHooks: { // 勾子对象里的props,在fix方法里会被添加进新事件 props: "char charCode key keyCode".split(" "), // filter也会在fix里被调用 filter: function( event, original ) { // 添加一个which属性 if ( event.which == null ) { // 使用原生事件的charCode或者keyCode event.which = original.charCode != null ? original.charCode : original.keyCode; } return event; } }, // 鼠标事件勾子 mouseHooks: { // 勾子对象里的props,在fix方法里会被添加进新事件 props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), filter: function( event, original ) { var body, eventDoc, doc, button = original.button, fromElement = original.fromElement; // 计算pageX/Y if ( event.pageX == null && original.clientX != null ) { eventDoc = event.target.ownerDocument || document; doc = eventDoc.documentElement; body = eventDoc.body; event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); } if ( !event.relatedTarget && fromElement ) { event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; } if ( !event.which && button !== undefined ) { event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); } return event; } }, // 特殊事件类型的一些属性跟方法,这些属性跟方法均在前面的jQuery.event中被使用到,应该可以看做是跟勾子类似的对象 special: { load: { // Prevent triggered image.load events from bubbling to window.load noBubble: true }, focus: { // Fire native event if possible so blur/focus sequence is correct trigger: function() { if ( this !== safeActiveElement() && this.focus ) { try { this.focus(); return false; } catch ( e ) { // Support: IE<9 // If we error on focus to hidden element (#1486, #12518), // let .trigger() run the handlers } } }, delegateType: "focusin" }, blur: { trigger: function() { if ( this === safeActiveElement() && this.blur ) { this.blur(); return false; } }, delegateType: "focusout" }, click: { // For checkbox, fire native event so checked state will be right trigger: function() { if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { this.click(); return false; } }, // For cross-browser consistency, don't fire native .click() on links _default: function( event ) { return jQuery.nodeName( event.target, "a" ); } }, beforeunload: { postDispatch: function( event ) { // Support: Firefox 20+ // Firefox doesn't alert if the returnValue field is not set. if ( event.result !== undefined && event.originalEvent ) { event.originalEvent.returnValue = event.result; } } } }, // simulate这个方法暂时也是不理解其用途 // 貌似是模仿一些具体类型的事件,然后触发 simulate: function( type, elem, event, bubble ) { var e = jQuery.extend( new jQuery.Event(), event, { type: type, isSimulated: true, originalEvent: {} } ); if ( bubble ) { jQuery.event.trigger( e, null, elem ); } else { jQuery.event.dispatch.call( elem, e ); } if ( e.isDefaultPrevented() ) { event.preventDefault(); } } }; // 删除事件监听,在jQuery.event.remove里会用到 jQuery.removeEvent = document.removeEventListener ? // 判断document.removeEventListener,存在说明支持则使用如下方法 function( elem, type, handle ) { if ( elem.removeEventListener ) { elem.removeEventListener( type, handle, false ); } } : // 不支持则使用IE的detachEvent方法 function( elem, type, handle ) { var name = "on" + type; if ( elem.detachEvent ) { // #8545, #7054以防自定义事件的内存泄露IE6-8 // IE解除事件绑定,需要节点上存在该事件属性 if ( typeof elem[ name ] === strundefined ) { elem[ name ] = null; } elem.detachEvent( name, handle ); } };

 

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

相关文章
  • 【代码片-1】 jQuery源码学习(版本1.11)-事件处理-实例函数

    【代码片-1】 jQuery源码学习(版本1.11)-事件处理-实例函数

    2017-05-28 10:09

  • jQuery使用注意点技巧2——jQuery 选择器详解

    jQuery使用注意点技巧2——jQuery 选择器详解

    2017-04-21 14:00

  • 你身边的技术百科全书

    你身边的技术百科全书

    2016-05-07 12:01

网友点评