如何检查绑定到元素的事件列表

问题描述 投票:0回答:2

我正在寻找一种方法来列出由 Javascript 添加到某个元素的所有事件。我知道

getEventListeners
在开发工具中可用,但在脚本中不可用。

我知道使用 jQuery,我可以使用下面的代码检查事件。然而,由于这是一个私有方法,它只能列出 jQuery 添加的事件。

$._data(element, 'events')

我也检查了这个页面,但是这个方法需要在添加事件之前执行。 https://www.sqlpac.com/en/documents/javascript-listing-active-event-listeners.html

毕竟,有没有通用的方法可以在添加事件后检查元素是否有事件?

javascript jquery-events
2个回答
0
投票

这是可能的,但前提是您使用自己的拦截器拦截/覆盖 addEventListener 和 removeEventListener 原型函数,这样您就可以拦截它们。

这适用于使用 addEventListener 添加的任何内容(并且它考虑了 removeEventListener)。

但是如果您在没有 EventListener 的情况下添加它们,例如使用 element.onclick (或在标记中的 onclick/onAnything-attribute 中),这不会列出它们,您必须手动检查它们。

确保以下 JavaScript 是您页面上执行的第一个脚本,否则可能无法正常工作。

方法如下(TypeScript):

type EventHandlerMapType = {
    // [key: EventTarget]: { [type: string]: EventListenerOrEventListenerObject[] };
    [key: string]: { [type: string]: EventListenerOrEventListenerObject[] };
};


type EventHandlerMapValue = { [type: string]: EventListenerOrEventListenerObject[] };
interface EventTarget
{
    getEventHandlers: (type?: string) => EventHandlerMapValue | EventListenerOrEventListenerObject[];
}





// function addEventListener<K extends keyof ElementEventMap>(type: K, listener: (this: Element, ev: ElementEventMap[K]) => any, options ?: boolean | AddEventListenerOptions): void;
// addEventListener(type: string, listener: EventListenerOrEventListenerObject, options ?: boolean | AddEventListenerOptions): void;


(function ()
{
    // Store the handlers by element reference
    // WeakMap can take an object, such as an Element, as a key, object cannot. 
    // This is useful because WeakMap allows for garbage collection of the keys(the elements), 
    // meaning when an Element is removed from the DOM and no longer referenced, it gets garbage - collected, 
    // and its entry in the WeakMap is automatically removed. 
    // This prevents memory leaks.
    const eventHandlerMap = new WeakMap<EventTarget>(); // Dictionary<Element, { type:[]}> // where type is string and array is an array of handlers/listeners

    // Override the native addEventListener
    const originalAddEventListener = EventTarget.prototype.addEventListener;

    
    
    EventTarget.prototype.addEventListener = function (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions)
    {
        // Call the original addEventListener to ensure normal behavior
        originalAddEventListener.call(this, type, listener, options);

        // Initialize tracking for the current element if it doesn't exist
        if (!eventHandlerMap.has(this))
        {
            eventHandlerMap.set(this, {});
        }

        // Get the event type handlers for this element
        const handlersForElement = eventHandlerMap.get(this);
        if (!handlersForElement[type])
        {
            handlersForElement[type] = [];
        }

        // Add the handler to the list for this event type
        handlersForElement[type].push(listener);
    };

    // Override the native removeEventListener
    const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
    EventTarget.prototype.removeEventListener = function (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions)
    {
        // Call the original removeEventListener to ensure normal behavior
        originalRemoveEventListener.call(this, type, listener, options);

        // Remove the handler from the tracking list
        if (eventHandlerMap.has(this))
        {
            const handlersForElement = eventHandlerMap.get(this);
            if (handlersForElement[type])
            {
                // Filter out the handler that matches the one being removed
                handlersForElement[type] = handlersForElement[type].filter((h: EventListenerOrEventListenerObject) => h !== listener);

                // Clean up if no handlers left for this event type
                if (handlersForElement[type].length === 0)
                {
                    delete handlersForElement[type];
                }
            }

            // Clean up the element if no handlers left for any event type
            if (Object.keys(handlersForElement).length === 0)
            {
                eventHandlerMap.delete(this);
            }
        }
    };

    // Function to retrieve all event handlers for an element
    EventTarget.prototype.getEventHandlers = function (type?: string): EventHandlerMapValue | EventListenerOrEventListenerObject[]
    {
        // Get the tracking list for the current element
        const handlersForElement = eventHandlerMap.get(this) || {};

        if (type)
        {
            // If a specific event type is requested, return its handlers
            return handlersForElement[type] || [];
        }

        // If no type is specified, return all handlers grouped by type
        return handlersForElement;
    };

})();

现在在 EventTarget(元素、节点等)上:

getEventHandlers(type?: string)

或者用普通的 JS

(function () {
    var eventHandlerMap = new WeakMap();
    var originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function (type, listener, options) {
        originalAddEventListener.call(this, type, listener, options);
        if (!eventHandlerMap.has(this)) {
            eventHandlerMap.set(this, {});
        }
        var handlersForElement = eventHandlerMap.get(this);
        if (!handlersForElement[type]) {
            handlersForElement[type] = [];
        }
        handlersForElement[type].push(listener);
    };
    var originalRemoveEventListener = EventTarget.prototype.removeEventListener;
    EventTarget.prototype.removeEventListener = function (type, listener, options) {
        originalRemoveEventListener.call(this, type, listener, options);
        if (eventHandlerMap.has(this)) {
            var handlersForElement = eventHandlerMap.get(this);
            if (handlersForElement[type]) {
                handlersForElement[type] = handlersForElement[type].filter(function (h) { return h !== listener; });
                if (handlersForElement[type].length === 0) {
                    delete handlersForElement[type];
                }
            }
            if (Object.keys(handlersForElement).length === 0) {
                eventHandlerMap.delete(this);
            }
        }
    };
    EventTarget.prototype.getEventHandlers = function (type) {
        var handlersForElement = eventHandlerMap.get(this) || {};
        if (type) {
            return handlersForElement[type] || [];
        }
        return handlersForElement;
    };
})();

测试:

var btnCreated = document.createElement("button");
btnCreated.textContent = "Hello Kitty";
btnCreated.value = "Hello Kitty";
document.body.appendChild(btnCreated);
var btn = document.querySelector('button');
function handleClick() {
    console.log('Button clicked');
}
btn.addEventListener('click', handleClick);
btn.addEventListener('clock', handleClick);
console.log(btn.getEventHandlers('click'));
console.log("before click");
btn.click();
console.log("after click");
btn.removeEventListener('click', handleClick);
console.log("before click after click removed");
btn.click();
console.log("after click after click removed");
console.log("click handlers", btn.getEventHandlers('click'));
console.log("all handlers", btn.getEventHandlers());

0
投票

Stefan Steiger 的 answer 是正确的,因为内置对象的

prototype
EventTarget
是最佳选择)必须重写
addEventListener()
(和
removeEventListener()
)方法才能找到它们。我的答案包括这一点以及列出所有属性处理程序的能力。下面的示例是代码的现代化版本here。示例中注释了详细信息。

/**
 * Override EventTarget.prototype addEventListener() method so when 
 * it's used an object (listenerList) is bound to the DOM node that is registered
 * to the given event.type (etype). For every event.type the node is registered
 * to it will have an array of objects (listenerList[eType]). Each object within those
 * arrays will have function (callback/handler) and capture (options/config) data.
 * @param {string} etype          - event.type
 * @param {function} callback     - The event handler function.
 * @param {object|boolean} config - Options as an object or capture as a boolean
 *                                  If undefined it defaults to false.
 */
const addEL = EventTarget.prototype.addEventListener;

EventTarget.prototype.addEventListener = function(etype, handler, config) {
  if (!config) config = false;
  addEL.call(this, etype, handler, config);
  if (!this.listenerList) this.listenerList = {};
  if (!this.listenerList[etype]) this.listenerList[etype] = [];
  this.listenerList[etype].push({
    callback: handler,
    options: config
  });
};

/**
 * Override EventTarget.prototype removeEventListener() method so when   it's used
 * the corresponding object nested within an array of objects (listenerList[evType]) that's nested within a node's bound object
 * (listenerList) is removed.
 * @param {string} etype          - event.type
 * @param {function} callback     - The event handler function.
 * @param {object|boolean} config - Options as an object or capture as a boolean
 *                                  If undefined it defaults to false.
 *
 */
const remEL = EventTarget.prototype.removeEventListener;

EventTarget.prototype.removeEventListener = function(etype, handler, config) {
  if (!config) config = false;
  remEL.call(this, etype, handler, config);
  if (!this.listenerList) this.listenerList = {};
  if (!this.listenerList[etype]) this.listenerList[etype] = [];

  this.listenerList[etype].forEach((listener, index) => {
    if (listener.callback == handler, listener.options == config) {
      this.listenerList[etype].splice(index, 1);
    }
  });
  if (this.listenerList[etype].length === 0) delete this.listenerList[etype];
};

/**
 * Add a method to EventTarget.prototype getListeners() which
 * returns the object (listenerList) in it's entirity unless
 * the @param etype is specified to which it will only return
 * the array of objects (listenerList[etype]) containing data
 * for etype only.
 * @param {string} etype  - event.type
 * @return {object|array} - If event.type is specified (etype)
 *                          then it's an array (listenerList[etype]).
 *                          If undefined then the main object is
 *                          returned (listenerList).
 */
EventTarget.prototype.getListeners = function(etype) {
  if (!this.listenerList) this.listenerList = {};
  if (!etype) {
    return this.listenerList;
  }
  return this.listenerList[etype];
};

/**
 * This will collect data on any:
 *   - attribute event handler 
 *       <div onclick="callback()">
 *   - property event handler
 *       div.onclick = callback
 *   - eventListener()
 *       div.addEventListener("click", callback)
 * on every DOM node on the page (including window, document, etc).
 * @return {array} - An array of objects, each object is like the following:
 *  { 
 *    node: htmlString of element,
 *    type: event.type,
 *    func: stringified callback function 
 *  }
 *                 <<<IMPORTANT>>>
 * In order to gather data on any listeners created by addEventListener()
 * EventTarget prototype addEventListener() must be overridden
 * with the previous code. Moreover that code must be placed  
 * before the other scripts as much as possible.
 */
function listListeners() {
  // Collect all DOM nodes into an array
  const domNodes = [window, document, ...document.querySelectorAll('*')];
  // Create an array of all on-attributes from the window object
  const types = Object.keys(window).filter(key => key.startsWith("on"));
  /**
   * Result is an array of objects, see comment above. 
   * flatMap() is used to return a single array (it flattens one level of arrays)
   * because there are three arrays to be processed:
   *   - Outer iteration of domNodes array each node will need to have it's
   *     on-attribute events (types) and eventListeners (events) listed
   *  - Inner iteration of types array for each node
   *  - Inner iteration of events array for each node
   */
  const listeners = domNodes.flatMap(node => {
    /**
     * Here flatMap() iterates through types array to find any
     * on-attributes that match with the current node. An object
     * is returned if there's a match, if not an empty array is returned
     * which becomes nothing (which is cleaner than an undefined).
     */
    const tags = types.flatMap(etype => {
      return typeof node[etype] === 'function' ? {
        node: node,
        type: etype,
        func: node[etype].toString()
      } : [];
    });
    /**
     * The new method getListeners() (see previously) returns the node's
     * object (listenerList). 
     */
    const events = node.getListeners();
    // Covert object (listenerList into an array of properties (keys)
    const evKeys = Object.keys(events);
    // If there's any event.types in evKeys array...
    if (evKeys.length > 0) {
      // for each event.types (evt) in evKeys array...
      evKeys.forEach(evt => {
        // find it (evt) in events array and extract it's data
        events[evt].forEach((_, idx) => {
          tags.push({
            node: node,
            type: evt,
            func: events[evt][idx].callback.toString()
          });
        });
      });
    }
    return tags;
  });
  return listeners.sort();
}

// For testing purposes.
const fc = document.forms[0].elements;

fc[0].oninput = test;
fc[1].addEventListener("change", test);
fc[3].onmouseenter = test;
Array.from(fc.rad).forEach(r => r.onchange = test);
fc[8].addEventListener("change", e => console.log(e.target.value));
fc[8].addEventListener("change", test);

console.log(listListeners());

function test(e) {
  console.log((e.target.type || e.target.tagName) + " triggered the " + e.type + " event");
}
.as-console-wrapper {
  left: auto !important;
  top: 0;
  width: 60%;
  min-height: 350px !important;
}

.as-console-row::after {
  display: none !important;
}
<form>
  <textarea>TEST</textarea><br><br>
  <label><input type="checkbox"> TEST</label><br><br>
  <button onclick="console.log('button triggered the click event')" type="button">TEST</button><br><br>
  <fieldset>
    <label><input name="rad" type="radio"> TEST</label>
    <label><input name="rad" type="radio"> TEST</label>
    <label><input name="rad" type="radio"> TEST</label>
    <label><input name="rad" type="radio"> TEST</label>
  </fieldset><br>
  <select>
    <option>TEST A</option>
    <option>TEST B</option>
    <option>TEST C</option>
    <option>TEST D</option>
  </select>
</form>
© www.soinside.com 2019 - 2024. All rights reserved.