我正在寻找一种方法来列出由 Javascript 添加到某个元素的所有事件。我知道
getEventListeners
在开发工具中可用,但在脚本中不可用。
我知道使用 jQuery,我可以使用下面的代码检查事件。然而,由于这是一个私有方法,它只能列出 jQuery 添加的事件。
$._data(element, 'events')
我也检查了这个页面,但是这个方法需要在添加事件之前执行。 https://www.sqlpac.com/en/documents/javascript-listing-active-event-listeners.html
毕竟,有没有通用的方法可以在添加事件后检查元素是否有事件?
这是可能的,但前提是您使用自己的拦截器拦截/覆盖 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());
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>