Ruby's Louvre

每天学习一点点算法

导航

jQuery源码学习笔记九

最近几天搞了一个基于事件代理的事件系统,但即便是事件代理还是要依赖于事件注册,因此深入研究了jQuery的事件系统,整理出来分享一下。

由于IE与标准浏览器闹别扭,我们通过虽然弄一个叫addEvent的函数来屏蔽差异。以下就是一个经典的addEvent函数:

var addEvent = function( obj, type, fn ) {
    if (obj.addEventListener)
        obj.addEventListener( type, fn, false );
    else if (obj.attachEvent) {
        obj["e"+type+fn] = fn;
        obj.attachEvent( "on"+type, function() {
            obj["e"+type+fn]();
        } );
    }
};

但这简洁函数有许多缺点,如不能处理IE下绑定的回调函数的执行顺序问题,也根本无法消除事件对象的差异。于是有

 
//http://dean.edwards.name/weblog/2005/10/add-event/
//http://dean.edwards.name/weblog/2005/10/add-event2/
function addEvent(element, type, handler) {
    // assign each event handler a unique ID
    //在每个回调函数上绑定了一个UUID
    if (!handler.$$guid) handler.$$guid = addEvent.guid++;
    // create a hash table of event types for the element
    //在要绑定事件的元素节点上设置一个特殊的属性,用来储存事件
    if (!element.events) element.events = {};
    // create a hash table of event handlers for each element/event pair
    //evets函数的键名为事件的类型名,或者说把事件按类型来按理
    var handlers = element.events[type];
    if (!handlers) {
        handlers = element.events[type] = {};
        // store the existing event handler (if there is one)
        if (element["on" + type]) {
            handlers[0] = element["on" + type];//DOM1.0
        }
    }
    // store the event handler in the hash table
    //让一个类型对应多个回调函数
    handlers[handler.$$guid] = handler;
    // assign a global event handler to do all the work
    element["on" + type] = handleEvent;
};
// a counter used to create unique IDs
addEvent.guid = 1;
 
function removeEvent(element, type, handler) {
    // delete the event handler from the hash table
    if (element.events && element.events[type]) {
        //移除当前类型对应的某个回调函数
        delete element.events[type][handler.$$guid];
    }
};
 
function handleEvent(event) {
    var returnValue = true;
    // grab the event object (IE uses a global event object)
    event = event || fixEvent(window.event);
    // get a reference to the hash table of event handlers
    var handlers = this.events[event.type];
    // execute each event handler
    for (var i in handlers) {
        this.$$handleEvent = handlers[i];
        if (this.$$handleEvent(event) === false) {
            returnValue = false;
        }
    }
    return returnValue;
};
 
function fixEvent(event) {
    // add W3C standard event methods
    event.preventDefault = fixEvent.preventDefault;
    event.stopPropagation = fixEvent.stopPropagation;
    return event;
};
fixEvent.preventDefault = function() {
    this.returnValue = false;
};
fixEvent.stopPropagation = function() {
    this.cancelBubble = true;
};

完美的解决了IE的执行顺序问题,并为IE的事件对象添加了两个方法preventDefault与stopPropagation,jQuery的事件系统就是基于它发展而来。下面jQuery1.0.1的代码:

 
event: {
    // Bind an event to an element
    // Original by Dean Edwards
    add: function (element, type, handler) {
        // For whatever reason, IE has trouble passing the window object
        // around, causing it to be cloned in the process
        if (jQuery.browser.msie && element.setInterval != undefined) element = window;
        // Make sure that the function being executed has a unique ID
        if (!handler.guid) handler.guid = this.guid++;
        // Init the element's event structure
        if (!element.events) element.events = {};
        // Get the current list of functions bound to this event
        var handlers = element.events[type];
        // If it hasn't been initialized yet
        if (!handlers) {
            // Init the event handler queue
            handlers = element.events[type] = {};
            // Remember an existing handler, if it's already there
            if (element["on" + type]) handlers[0] = element["on" + type];
        }

        // Add the function to the element's handler list
        handlers[handler.guid] = handler;

        // And bind the global event handler to the element
        element["on" + type] = this.handle;
        //上面基本和DC大神的一致
        // Remember the function in a global list (for triggering)
        if (!this.global[type]) this.global[type] = [];
        this.global[type].push(element);
    },

    guid: 1,
    global: {},

    // Detach an event or set of events from an element
    remove: function (element, type, handler) {
        if (element.events) if (type && element.events[type]) if (handler) delete element.events[type][handler.guid];
        else for (var i in element.events[type]) delete element.events[type][i];
        else for (var j in element.events) this.remove(element, j);
    },
    //触发,为什么不叫fire呢?!
    trigger: function (type, data, element) {
        // Touch up the incoming data
        data = data || [];

        // Handle a global trigger
        if (!element) {
            var g = this.global[type];
            if (g) for (var i = 0; i < g.length; i++) this.trigger(type, data, g[i]);
            // Handle triggering a single element
        } else if (element["on" + type]) {
            // Pass along a fake event
            data.unshift(this.fix({
                type: type,
                target: element
            }));
            // Trigger the event
            element["on" + type].apply(element, data);
        }
    },

    handle: function (event) {
        if (typeof jQuery == "undefined") return;
        event = event || jQuery.event.fix(window.event);
        // If no correct event was found, fail
        if (!event) return;
        var returnValue = true;
        var c = this.events[event.type];
        for (var j in c) {
            if (c[j].apply(this, [event]) === false) {
                event.preventDefault();
                event.stopPropagation();
                returnValue = false;
            }
        }
        return returnValue;
    },

    fix: function (event) {
        if (event) {
            event.preventDefault = function () {
                this.returnValue = false;
            };
            event.stopPropagation = function () {
                this.cancelBubble = true;
            };
        }
        return event;
    }
}

我们来看一个这个经典的基于事件注册的事件系统,几年前主流的事件系统基于是这个样子。首先设置一个或几个顶层对象,用于管理事件句柄与相关的东西,这里正如我们看到的那样,是用一个叫global的对象。它装载的是元素,因为它是基于事件注册,回调函数都是直接绑定在元素上,后来IE7把内存泄漏的问题放大后,jQuery进一步改进,在unload时把这些注册了事件的元素上面事件全部去掉,现在还没有。在这些元素上有一个叫events的自定义属性,它是一个对象,按事件类弄型管理绑定在它上面的回调函数,目的是让事件按绑定时的顺序执行。当我们触发事件时,并不是直接执行我们绑定的回调函数,因为这里用的是DOM0的事件方式,无论绑定多少个同类型的事件,最后都只一个此类型的。因此都把它们放到一个handle函数中(DE大神的handleEvent)。handle做了三件事,让事件对象总是作为函数的第一个参数,改造事件对象,按顺序执行既定的回调函数。最后我们留意到它有返回值,这是用来决定它是否执行浏览器的默认行为。

我没有闲情把它所有的版本都看了,只看了几个版本,jQuery1.0.4基于还是那个样子。到jQuery1.1增加了几种绑定方式,著名的one,同时对toggle ,hover与ready进行大幅改进。在jQuery中,一个方法基本上都有两个版本,一个jQuery命名空间的静态方法,另一个是jQuery对象的实例方法。实例方法都是对应复数个元素(因为一个jQuery往往包含几个DOM元素),而静态方法基于上是对应一个。实例方法都是往外围调用这些静态方法,因此静态方法的地位相当高。改进的重点都是这些静态方法,因此我重点讲它们。有兴趣可以下jQuery1.1来看看,这时John Resig开始着手解决事件对象的差异问题,为IE的事件对象添加了pageX与pageY与target 属性。unbind与one事件实现得相当傻瓜,因为事件都是用顶层对象管理,把顶层对象的事件删掉,就是unbind了,删除后用一个拷收贝继续执行就是one。hover由于还没有事先搞定relatedTarget ,因此有点复杂。

由于事件系统是个复杂的东西,我还没有开始讲jQuery最新版本的情形,就已达这样的篇幅了。最后我总结下jQuery的事件运行流程吧:用户为元素绑定事件(bind)=>add=>为回调函数设置UUID=>交由顶层对象管理=>handle=>fix=>开始等待用户触发事件或直接调用trigger 。

posted on 2009-12-19 16:33  司徒正美  阅读(3199)  评论(6编辑  收藏  举报