29

I have Javascript that people are including in their page. In my Javascript I have a version of jQuery (1.8 for sake of easy reference) that is sectioned off into its own namespace, and referenced via a global variable (but not one of the two default vars of "$" or "jQuery"). This allows users to have jQuery in their page and have it not interfere with the stuff I'm doing internally in my functions.

So we have one page that has jQuery already (1.4), and everything works fine, except that the user and my code are both listening to "click" events on elements, and theirs is going first, so on the few events they do that return false, jQuery stops propagation and my event never gets triggered. I need my event to go first. The user is expecting my onClick functionality to still work.

Now I know that jQuery keeps its own order of events internally through the _data() object, and through this it is possible to unbind existing events, bind my event, then rebind the existing events, but that only applies to objects bound through that instance of jQuery. I'd rather not just blindly look for the jQuery object in hopes that the conflict was introduced by a user's own version of jQuery. After all what happens when a user binds the event not through jQuery? Trying to manipulate the existing jQuery object in the page isn't a good solution.

I know that, depending on browser, they are using addEventListener/removeEventListener or attachEvent/detachEvent. If only I could get a listing of the already added events, I could rebind them in the order I wanted, but I can't find out how. Looking through the DOM via Chrome inspect I don't see onclick bound anywhere (not on the object, not on window or document either).

I'm having the darndest time trying to figure out just exactly where jQuery binds its listening. To be able to control the order of its own events, jQuery must blanketly listen somewhere and then fire off its own functions right? If I could figure out where that's done I might get some insight into how to ensure my event is always first. Or maybe there's some Javascript API I haven't been able to find on Google.

Any suggestions?

4

7 回答 7

24

We solved this by just adding a little jQuery extension that inserts events at the head of the event chain:

$.fn.bindFirst = function(name, fn) {
  var elem, handlers, i, _len;
  this.bind(name, fn);
  for (i = 0, _len = this.length; i < _len; i++) {
    elem = this[i];
    handlers = jQuery._data(elem).events[name.split('.')[0]];
    handlers.unshift(handlers.pop());
  }
};

Then, to bind your event:

$(".foo").bindFirst("click", function() { /* Your handler */ });

Easy peasy!

于 2012-12-20T20:50:22.833 回答
13

As Bergi and Chris Heald said in the comments, it turns out there's no way to get at the existing events from the DOM, and no method to insert events "first". They are fired in the order they were inserted by design, and hidden by design. As a few posters mentioned you have access to the ones added through the same instance of jQuery that you're using via jQuery's data, but that's it.

There is one other case where you can run before an event that was bound before your code ran, and that's if they used the "onclick" HTML attribute. In that case you can write a wrapper function, as nothingisnecessary pointed out in a rather over-the-top toned comment below. While this wouldn't help in the instance of the original question I asked, and it's now very rare for events to be bound this way (most people and frameworks use addEvent or attachEventListener underneath now), it is one scenario in which you can solve the issue of "running first", and since a lot of people visit this question looking for answers now, I thought I'd make sure the answer is complete.

于 2012-12-28T15:26:01.590 回答
3

I encounter an opposite situation where I was asked to include a library, which uses event.stopImmediatePropagation() on an element, to our website. So some of my event handlers are skipped. Here is what I do (as answered here):

<span onclick="yourEventHandler(event)">Button</span>

Warning: this is not the recommended way to bind events, other developers may murder you for this.

于 2016-02-18T04:05:25.263 回答
2

Its not a proper solution, but ... You can add event handler to parent node in capture phase. Not on target element itself!

<div>
    <div id="target"></div>
</div>

target.parentNode.addEventListener('click',()=>{console.log('parent capture phase handler')},true)

Third argument in addEventListener means:

  • true - capture phase
  • false - bubble phase

Helpful links: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener

https://javascript.info/bubbling-and-capturing

于 2021-03-15T08:30:55.887 回答
1

Found it easiest to add addListener and removeListener methods to document (as that's only where I need them - I suppose you can use Element.prototype and this instead). Only one "real" listener is added per type, and it's just a func to call the actual listeners in order. The eventListeners dictionary is added to document (so can mess with the handler or order).

[edit] I think the correct answer for most cases is to use the 3rd argument of addEventListener: https://stackoverflow.com/a/29923421. The answer below ignores the argument (on purpose).

[edit] Updated code to only add one extra property: document.eventHandlers + modified naming.

// Storage.
document.eventListeners             = {};   // { type: [ handlerFunc, listenerFuncs ] }

// Add event listener - returns index.
document.addListener                = (type, listener, atIndex) => {
    // Get info.
    const listening                 = document.eventListeners[type];
    // Add to existing.
    if (listening) {
        // Clean up.
        atIndex                     = atIndex || 0;
        const listeners             = listening[1];     // Array of funcs.
        // Already has.
        const iExists               = listeners.indexOf(listener);
        if (iExists !== -1) {
            // Nothing to do.
            if (iExists === atIndex)
                return              atIndex;
            // Remove from old position.
            listeners.splice(atIndex, 1);
        }
        // Add (supporting one cycle of negatives).
        const nListeners            = listeners.length;
        if (atIndex > nListeners)
            atIndex                 = nListeners;
        else if (atIndex < 0)
            atIndex                 = Math.max(0, atIndex + nListeners + 1);
        listeners.splice(atIndex, 0, listener);
    }
    // New one.
    else {
        // Handler func.
        const handler               = (...args) => {
            const listening         = document.eventListeners[type];
            if (listening) {
                const listeners     = listening[1];     // Array of funcs.
                for (const listener of listeners)
                    listener(...args);
            }
        };
        // Update dictionary.
        document.eventListeners[type]   = [ handler, [ listener ] ];
        // Add listener.
        document.addEventListener(type, handler);
        // First one.
        atIndex                     = 0;
    }
    // Return index.
    return                          atIndex;
};

// Remove event listener - returns index (-1 if not found).
document.removeListener             = (type, listener) => {
    // Get info.
    const listening                 = document.eventListeners[type];
    if (!listening)
        return                      -1;
    // Check if exists.
    const listeners                 = listening[1];
    const iExists                   = listeners.indexOf(listener);
    if (iExists !== -1) {
        // Remove listener.
        listeners.splice(iExists, 1);
        // If last one.
        if (!listeners.length) {
            // Remove listener.
            const handlerFunc       = listening[0];
            document.removeEventListener(type, handlerFunc);
            // Update dictionary.
            delete                  document.eventListeners[type];
        }
    }
    // Return index.
    return                          iExists;
}
于 2020-11-16T22:30:29.483 回答
1

Aliaksei Pavlenkos suggestion about useCapture can be used. His allegation that it must be attached to the parent node is wrong: MDN

Event listeners in the “capturing” phase are called before event listeners in any non-capturing phases

target.addEventListener(type, listener, useCapture);
于 2021-06-05T09:34:49.780 回答
0

Just so it's said, I think this might be possible if you override the native implementations of these functions. This is BAD practice - very bad practice when developing a library to alter native implementations, because it can easily conflict with other libraries.

However, for completeness, here's one possibility (completely untested, just demonstrating the general concept):

// override createElement()
var temp = document.createElement;
document.createElement = function() {
    // create element
    var el = document.createElement.original.apply(document, arguments);

    // override addEventListener()
    el.addEventListenerOriginal = el.addEventListener;
    el._my_stored_events = [];

    // add custom functions
    el.addEventListener = addEventListenerCustom;
    el.addEventListenerFirst = addEventListenerFirst;
    // ...
};
document.createElement.original = temp;

// define main event listeners
function myMainEventListeners(type) {
    if (myMainEventListeners.all[type] === undefined) {
        myMainEventListeners.all[type] = function() {
            for (var i = 0; i < this._my_stored_events.length; i++) {
                var event = this._my_stored_events[i];
                if (event.type == type) {
                    event.listener.apply(this, arguments);
                }
            }
        }
    }
    return myMainEventListeners.all[type];
}
myMainEventListeners.all = {};

// define functions to mess with the event list
function addEventListenerCustom(type, listener, useCapture, wantsUntrusted) {
    // register handler in personal storage list
    this._my_stored_events.push({
        'type' : type,
        'listener' : listener
    });

    // register custom event handler
    if (this.type === undefined) {
        this.type = myMainEventListeners(type);
    }
}

function addEventListenerFirst(type, listener) {
    // register handler in personal storage list
    this._my_stored_events.push({
        'type' : type,
        'listener' : listener
    });

    // register custom event handler
    if (this.type === undefined) {
        this.type = myMainEventListeners(type);
    }
}

// ...

A lot more work would need to be done in this regard to truly lock this down, and again, it's best not to modify native libraries. But it's a useful mental exercise that helps to demonstrate the flexibility JavaScript provides in solving problems like this.

于 2015-04-06T22:25:20.107 回答