【jq 源码解析5-事件】

jq源码 | 2020-12-28 15:59:19 163次 0次

事件入口

jq 提供的事件操作 api 很多,但是基本都是依赖 on 方法为入口进行处理。

jQuery.each(
    ( "blur focus focusin focusout resize scroll click dblclick " +
    "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
    "change select submit keydown keypress keyup contextmenu" ).split( " " ),
    function( _i, name ) {
        jQuery.fn[ name ] = function( data, fn ) {
            return arguments.length > 0 ?
                this.on( name, null, data, fn ) :
                this.trigger( name );
        };
    }
);

其他方法:

jQuery.fn.extend( {

    on: function( types, selector, data, fn ) {
        return on( this, types, selector, data, fn );
    },
    ...
    off: function( types, selector, fn ) {
        ...
        return this.each( function() {
            jQuery.event.remove( this, types, fn, selector );
        } );
    }
} );
jQuery.fn.extend( {
    trigger: function( type, data ) {
        return this.each( function() {
            jQuery.event.trigger( type, data, this );
        } );
    },
    ...
} );

下面介绍一个 click 事件的执行流程:

on

 |

jQuery.event.add

 |

jQuery.event.dispatch

 |

jQuery.event.fix

 |

jQuery.event.handlers

 |

run callback

on

function on( elem, types, selector, data, fn, one ) {
    ...
    return elem.each( function() {
        jQuery.event.add( this, types, fn, data, selector );
    } );
}


jQuery.event.add

事件的添加和缓存,不会直接给 dom 添加事件:

jQuery.event = {
    add: function( elem, types, handler, data, selector ) {
        ...
        // 缓存中取
        elemData = data_priv.get( elem );
        ...
        // 事件标识
        if ( !handler.guid ) {
            handler.guid = jQuery.guid++;
        }
    
        // 同一个dom中的多个事件,避免重复初始化
        if ( !( events = elemData.events ) ) {
            events = elemData.events = Object.create( null );
        }
        if ( !( eventHandle = elemData.handle ) ) {
            eventHandle = elemData.handle = function( e ) {
    
                // !!!!!!事件监听被触发
                return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
                    jQuery.event.dispatch.apply( elem, arguments ) : undefined;
            };
        }
        ...
        while ( t-- ) {
            ...
            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 );
    
            ...
                // handlers \ events \ elemData 有引用
                handlers = events[ type ] = [];
                ...
                    // !!!!!监听,点击时执行 jQuery.event.dispatch
                    elem.addEventListener( type, eventHandle );
                ...
            ...
            if ( selector ) {
                // 元素被委托事件
                // 并且委托元素事件排序在普通事件的前面
                handlers.splice( handlers.delegateCount++, 0, handleObj );
            } else {
                handlers.push( handleObj );
            }
        }
    
    },
}


jQuery.event.dispatch

上面的过程中完成了事件的处理和绑定,给元素绑定了事件监听,但监听的事件不是直接执行传入的事件回调,而是一个jq内部中的 dispatch,这里做三件事:

修正 event(目前看感觉没有必要

解析出 handlerQueue 事件集合

执行

dispatch: function( nativeEvent ) {

    ...
    event = jQuery.event.fix( nativeEvent ),

    ...
    // 解析
    handlerQueue = jQuery.event.handlers.call( this, event, handlers );

    // 执行  判断阻止冒泡
    i = 0;
    while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {
        event.currentTarget = matched.elem;

        j = 0;
        while ( ( handleObj = matched.handlers[ j++ ] ) &&
            !event.isImmediatePropagationStopped() ) {
                ...
                // 执行传入的回调函数
                ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||
                    handleObj.handler ).apply( matched.elem, args );
                
                // 阻止默认事件和冒泡
                if ( ret !== undefined ) {
                    if ( ( event.result = ret ) === false ) {
                        event.preventDefault();
                        event.stopPropagation();
                    }
                }
            }
        }
    }
    ...
    return event.result;
},

以上是一个基本的事件注册和执行流程,下面介绍一下事件委托的设计。


事件委托

比如一些动态创建的节点,直接绑定事件是无效的,因为 dom 没有渲染,jq 的选择器找不到,只能使用事件委托的形势,因为事件委托是一个事件监听父元素的点击事件,通过 target 判断进行回调方法的执行,有关委托的处理在 jQuery.event.handlers 里:

handlers: function( event, handlers ) {
    ...
        // target获取  点击的元素
        cur = event.target;

    // 委托事件 delegateCount 在add方法中 判断如果事件中传入了一个选择器(被委托)则进行++处理
    if ( delegateCount ) {
        // cur !== this 判断点击的元素和委托的元素是否相同
        // 并且一直往上找  直到找到被委托的目标
        for ( ; cur !== this; cur = cur.parentNode || this ) {
            if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) {
                matchedHandlers = [];
                matchedSelectors = {};
                // 委托事件个数
                for ( i = 0; i < delegateCount; i++ ) {
                    handleObj = handlers[ i ];

                    // 选择器
                    sel = handleObj.selector + " ";
                    
                    // 选择dom
                    if ( matchedSelectors[ sel ] === undefined ) {
                        matchedSelectors[ sel ] = handleObj.needsContext ?
                            jQuery( sel, this ).index( cur ) > -1 :
                            jQuery.find( sel, this, null, [ cur ] ).length;
                    }
                    // 事件回调函数存储
                    if ( matchedSelectors[ sel ] ) {
                        matchedHandlers.push( handleObj );
                    }
                }
                if ( matchedHandlers.length ) {
                    handlerQueue.push( { elem: cur, handlers: matchedHandlers } );
                }
            }
        }
    }

    // 普通的事件
    cur = this;
    if ( delegateCount < handlers.length ) {
        handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } );
    }
    
    return handlerQueue;
},

大致流程还是比较清晰,其中需要注意的是,在 add 方法中 handlers 进行事件存储时,事件委托的顺序是在普通事件之前的,这样做的好处就是默认事件就冒泡了,不需要其他的处理,如下:

$('.par').click(() => {
    console.log(222222)
})
$('.par').on('click', '.child', (e) => {	
    console.log(111111)
    e.stopPropagation();
})

情况一:不加组织冒泡事件,结果为 111111 再是 222222

情况二:加上组织冒泡,由于事件委托事件排序在前,先执行,执行完,e对象中加了 stopPropagation,这个 e 是 jq 内部中共用的一个对象,所以下个事件执行时会判断 event.isPropagationStopped() 值,决定是否继续执行,这样就成功的阻止了冒泡。

事件系统的大体设计流程结束,其他的还有很多细节处理,这里不再展开。

0人赞

分享到: