我正在编写一个Javascript库,我想知道是否可以执行以下操作。
我想在元素上触发自定义事件,但我不知道先验地订阅了该事件的事件处理程序,也不知道订阅了多少个。 然后,我想等待所有这些事件处理程序完成,然后检查它们中是否有任何执行了给定的操作(例如"拒绝"事件)。如果没有,则触发事件的函数将继续。
需要明确的是,我可以为事件处理程序提供参数,例如"() => reject()"函数,或者为事件处理程序定义任何类型的"合约",但我不能修改订阅事件处理程序的代码。此类代码将由库的用户编写。
这可能/可取吗?
谢谢!
更新
这是我想使用的代码狙击器的示例,考虑到库最终用户基本上会自己调用addEventListener()或$.on()
$body = $("body")
function rejectEvent(o) {
o.reject();
}
function acceptEvent(o) {}
function triggerEvent() {
let isRejected = false;
$body.trigger('custom-event', {
reject: () => isRejected = true;
});
// Wait for all event handlers to complete...
if (isRejected) {
console.log('stop');
} else {
console.log('proceed');
}
}
triggerEvent(); // Should display 'proceed'
$body.on('custom-event', function(e, o) {
console.log('do nothing');
});
triggerEvent(); // Should display 'do nothing' then 'proceed'
$body.on('custom-event', function(e, o) {
console.log('reject');
o.reject();
});
triggerEvent(); // Should display 'do nothing' then 'reject' then 'stop'
$body.off('custom-event');
$body.on('custom-event', async function(e, o) {
setTimeout(() => {
console.log('reject');
o.reject();
}, 5000);
});
triggerEvent(); // Should display 'proceed' then 'reject'
如此示例所示,只要事件处理程序是同步执行的,我就可以正确检索事件处理程序的拒绝状态(至少我从谷歌搜索这个主题中了解到的)。 但是,我遇到的主要问题是最终用户是否将事件处理程序定义为异步。
到目前为止,我能看到的最佳选择是记录不支持异步事件处理程序,但我希望也能够支持它们。
从上面的评论...
"@PeterSeliger更改addEventListener方法听起来很有趣。我想这样做是有意义的,并尝试将最终用户事件处理程序(可能是异步的)包装在同步函数中,并确保用户事件处理程序在包装函数终止之前完成。会给它一些想法,但我欢迎更多的建议。
半句话..."同步函数中的最终用户事件处理程序(可能是异步的)">......让我想知道是否应该将经典(即发即弃)事件处理与承诺和/或异步等待语法混合在一起。实际上,直到现在,我从未遇到过任何承诺/异步事件处理程序。如果将后者(异步事件处理程序函数)与调度自定义事件混合在一起,则必须将原型
dispatchEvent
替换为自己的基于异步函数的实现,或者必须为其提出自己的原型额外方法。– 彼得·塞利格
当然,这是可以做到的。
一种是HTMLElement.prototype.addEventListener
的拦截方法开始,其中将围绕原始实现包装其他功能。因此,后来还能够通过用自己的实现替换HTMLElement.prototype.dispatchEvent
或提出自定义来控制自定义事件的调度,例如HTMLElement.prototype.dispatchMonitoredEvent
方法,或者更好的是,如果完全控制调度环境,则将其实现为lib-author可访问dispatchMonitoredEvent
方法。
至于拦截方法。这样做是为了将特定于元素节点的侦听器存储在(甚至可能是全局可访问的)listenerStorage
中,该是一个WeakMap
实例,它基于节点引用,将每个节点的侦听器Map
实例中保存。后者具有特定于事件类型的条目,其中可以通过事件类型键访问处理程序数组值。
function handleFormControlEvent({ type: eventType, currentTarget: node }) {
console.log({
node,
nodeValue: node.value,
eventType,
});
}
function logFormControlHandlerCount({ type: eventType, currentTarget: node }) {
console.log({
[ `${ eventType }HandlerCount` ]: listenerStorage
.get(node)
.get(eventType)
.length
});
}
function logCheckboxState({ currentTarget: { checked } }) {
console.log({ checked });
}
document
.querySelectorAll('input')
.forEach(elmNode => {
elmNode.addEventListener('input', handleFormControlEvent);
elmNode.addEventListener('input', logFormControlHandlerCount);
});
document
.querySelectorAll('[type="checkbox"]')
.forEach(elmNode => {
elmNode.addEventListener('click', handleFormControlEvent);
elmNode.addEventListener('click', logCheckboxState);
elmNode.addEventListener('click', logFormControlHandlerCount);
});
document
.querySelector('[type="checkbox"]')
.dispatchEvent(new CustomEvent('click'));
body { margin: 0; }
fieldset { width: 40%; margin: 0; padding: 0 0 4px 4px; }
.as-console-wrapper { left: auto!important; min-height: 100%!important; width: 58%; }
<fieldset>
<legend>test area</legend>
<label>
<span>Foo:</span>
<input type="text" value="Foo" />
</label>
<label>
<span>Bar:</span>
<input type="checkbox" />
</label>
</fieldset>
<script>
HTMLElement.prototype.addEventListener = (function (proceed) {
window.listenerStorage = storage = new WeakMap;
return function addEventListener(type, handler) {
const currentTarget = this;
let listeners = storage.get(currentTarget);
if (!listeners) {
listeners = new Map;
storage.set(currentTarget, listeners);
}
let handlerList = listeners.get(type);
if (!handlerList) {
handlerList = [];
listeners.set(type, handlerList);
}
if (!handlerList.includes(handler)) {
handlerList.push(handler)
}
// proceed with delegation to the original implementation.
proceed.call(currentTarget, type, handler);
};
}(HTMLElement.prototype.addEventListener));
</script>
监视取决于处理程序函数,该函数必须返回一个值(这不是经典的处理程序函数方式),从中可以判断失败/成功,或者依赖于已解决/已解决的异步处理程序函数的返回值(这更不寻常)。基于上面的示例代码,非/原型异步dispatchMonitoredEvent
函数/方法的实现和使用可能如下所示......
function handleFormControlEvent({ type: eventType, currentTarget: node }) {
console.log({
node,
nodeValue: node.value,
eventType,
});
// function statement as event handler, but with unusual return value.
return { controlEventSuccess: true };
}
function logFormControlHandlerCount({ type: eventType, currentTarget: node }) {
console.log({
[ `${ eventType }HandlerCount` ]: listenerStorage
.get(node)
.get(eventType)
.length
});
// function statement as event handler, but with unusual return value.
return { handlerCountSuccess: true };
}
async function logCheckboxState({ currentTarget: { checked } }) {
// asynchronous function as event handler.
return new Promise(resolve =>
setTimeout(() => {
console.log({ checked });
resolve({ checkboxStateSuccess: true });
}, 2000)
);
}
document
.querySelectorAll('input')
.forEach(elmNode => {
elmNode.addEventListener('input', handleFormControlEvent);
elmNode.addEventListener('input', logFormControlHandlerCount);
});
document
.querySelectorAll('[type="checkbox"]')
.forEach(elmNode => {
elmNode.addEventListener('click', handleFormControlEvent);
elmNode.addEventListener('click', logCheckboxState);
elmNode.addEventListener('click', logFormControlHandlerCount);
});
document
.querySelector('[type="checkbox"]')
.dispatchEvent(new CustomEvent('click'));
console.log(
'n+++ dispatched monitored custom events +++nn'
);
(async function () {
console.log(
'... trigger ... execute all at once and log at the end ...'
);
// execute all at once ...
Promise
.all([
document
.querySelector('[type="checkbox"]')
.dispatchMonitoredEvent(new CustomEvent('click')),
document
.querySelector('input')
.dispatchMonitoredEvent(new CustomEvent('input')),
])
// ... and log at the end.
.then(values =>
values.forEach(returnValues =>
console.log({ returnValues })
)
);
console.log(
'... trigger ... execute and log one after the other ...'
);
// execute and log one after the other.
let returnValues = await document
.querySelector('[type="checkbox"]')
.dispatchMonitoredEvent(new CustomEvent('click'));
console.log({ returnValues });
returnValues = await document
.querySelector('input')
.dispatchMonitoredEvent(new CustomEvent('input'));
console.log({ returnValues });
})();
body { margin: 0; }
fieldset { width: 40%; margin: 0; padding: 0 0 4px 4px; }
.as-console-wrapper { left: auto!important; min-height: 100%!important; width: 58%; }
<fieldset>
<legend>test area</legend>
<label>
<span>Foo:</span>
<input type="text" value="Foo" />
</label>
<label>
<span>Bar:</span>
<input type="checkbox" />
</label>
</fieldset>
<script>
HTMLElement.prototype.addEventListener = (function (proceed) {
window.listenerStorage = storage = new WeakMap;
return function addEventListener(type, handler) {
const currentTarget = this;
let listeners = storage.get(currentTarget);
if (!listeners) {
listeners = new Map;
storage.set(currentTarget, listeners);
}
let handlerList = listeners.get(type);
if (!handlerList) {
handlerList = [];
listeners.set(type, handlerList);
}
if (!handlerList.includes(handler)) {
handlerList.push(handler)
}
// proceed with delegation to the original implementation.
proceed.call(currentTarget, type, handler);
};
}(HTMLElement.prototype.addEventListener));
async function dispatchMonitoredEvent({
isTrusted =false, bubbles = false,
cancelBubble = false, cancelable = false,
composed = false, defaultPrevented = false,
detail = null, eventPhase = 0, path = [],
returnValue = true, timeStamp = Data.now(),
type = null,
}) {
// @TODO ... custom event data handling/copying is in need of improvement.
const currentTarget = this;
const monitoredEvent = {
isTrusted, bubbles, cancelBubble, cancelable, composed, currentTarget,
defaultPrevented, detail, eventPhase, path, returnValue,
srcElement: currentTarget, target: currentTarget,
timeStamp, type,
};
const handlerList = listenerStorage
.get(currentTarget)
?.get(type) ?? [];
return (await Promise
.all(
handlerList
.map(handler =>
handler(monitoredEvent)
// (async (evt) => handler(evt))(monitoredEvent)
)
));
};
HTMLElement
.prototype
.dispatchMonitoredEvent = dispatchMonitoredEvent;
</script>
编辑
从上述方法和实现中学习,现在考虑到OP后来提供的示例代码,可以提出自定义可取消事件的实现,其中整个async...await
处理都是围绕自定义事件的增强detail
属性构建的。
因此,无论是否注册了普通/经典或异步处理程序函数,都不会更改处理程序的 arity(函数的预期/要处理的参数的数量),而是通过设置例如将唯一事件参数的detail.proceed
属性设置为false
来控制调度过程的取消。
元素的所有已注册事件类型特定处理程序函数都将由异步生成器处理,该生成器控制函数执行并有权访问当前事件对象的引用。根据失败/拒绝的处理程序函数或当前event.detail.proceed
值,异步生成器继续/停止生成。
操作生成器的异步dispatchCustomCancelableEvent
方法确实返回一个对象,该对象携带有关已结算调度过程的所有相关数据,如success
、canceled
、error
和event
。event.detail
提供了有关所涉及的handlers
和cancelHandler
(负责任何类型的取消的处理程序)的其他信息。
async function triggerTestEvent(elmNode) {
const result = await elmNode
.dispatchCustomCancelableEvent('custom-cancelable-event');
const { /*event, success, */canceled/*, error = null*/ } = result;
// in order to meet the OP's requirement of ...
// ... "Wait for all event handlers to complete"
if (canceled) {
console.log('stop', { result });
} else {
console.log('proceed', { result });
}
}
(async () => {
const testNode = document.querySelector('div');
// should display 'proceed'.
await triggerTestEvent(testNode);
testNode
.addEventListener('custom-cancelable-event', () =>
console.log('do nothing')
);
// should display 'do nothing' then 'proceed'.
await triggerTestEvent(testNode);
testNode
.addEventListener('custom-cancelable-event', evt => {
console.log('reject');
evt.detail.proceed = false; // was ... o.reject();
});
testNode
.addEventListener('custom-cancelable-event', () =>
console.log('+++ should never be displayed +++')
);
// should display 'do nothing' then 'reject' then 'stop'
await triggerTestEvent(testNode);
testNode
.removeCustomListeners('custom-cancelable-event');
testNode
.addEventListener('custom-cancelable-event', (/*evt*/) =>
console.log('... handle event ...')
);
testNode
.addEventListener('custom-cancelable-event', async (evt) =>
new Promise((resolve/*, reject*/) =>
setTimeout(() => {
console.log('... cancle event ...');
// reject('cancle event');
evt.detail.proceed = false; // was ... o.reject();
resolve();
}, 5000)
)
);
testNode
.addEventListener('custom-cancelable-event', () =>
console.log('+++ should never be displayed +++')
);
// should display
// '... handle event ...' then '... cancle event ...' then 'stop'
await triggerTestEvent(testNode);
})();
body { margin: 0; }
.as-console-wrapper { left: auto!important; min-height: 100%!important; width: 80%; }
<div>dispatch test node</div>
<script>
HTMLElement.prototype.addEventListener = (function (proceed) {
window.listenerStorage = storage = new WeakMap;
return function addEventListener(type, handler) {
const currentTarget = this;
let listeners = storage.get(currentTarget);
if (!listeners) {
listeners = new Map;
storage.set(currentTarget, listeners);
}
let handlerList = listeners.get(type);
if (!handlerList) {
handlerList = [];
listeners.set(type, handlerList);
}
if (!handlerList.includes(handler)) {
handlerList.push(handler)
}
// proceed with delegation to the original implementation.
proceed.call(currentTarget, type, handler);
};
}(HTMLElement.prototype.addEventListener));
HTMLElement.prototype.removeCustomListeners =
function removeCustomListeners (type) {
storage
.get(this)
?.delete?.(type);
};
HTMLElement.prototype.dispatchCustomCancelableEvent = (function () {
async function* createCancelableDispatchablesPool(handlerList, evt) {
handlerList = [...handlerList];
let isProceed = true;
let handler;
let recentHandler;
while (isProceed && (handler = handlerList.shift())) {
try {
if (evt.detail.proceed === true) {
recentHandler = handler;
yield (await handler(evt));
} else {
evt.detail.proceed = isProceed = false;
evt.detail.cancelHandler = recentHandler;
}
} catch (reason) {
// an (async) handler function's execution
// could also just fail with or without reason.
evt.detail.proceed = isProceed = false;
evt.detail.cancelHandler = recentHandler;
yield (
new Error(String(reason ?? 'failed without reason'))
);
}
}
}
return async function dispatchCustomCancelableEvent(type, options = {}) {
const currentTarget = this;
const handlerList = listenerStorage
.get(currentTarget)
?.get(type) ?? [];
Object.assign((options.detail ??= {}), {
currentTarget,
target: currentTarget,
// extend a custom event's `detail`
// by a boolean `proceed` property.
// and a list of all event `handlers`.
proceed: true,
handlers: handlerList,
});
const customEvent = new CustomEvent(type, options);
const dispatchablesPool =
createCancelableDispatchablesPool(handlerList, customEvent);
const dispatchResult = {
success: true,
canceled: true,
};
for await (const result of dispatchablesPool) {
// an (async) handler function's execution
// could also just fail with or without reason.
if (result instanceof Error) {
dispatchResult.success = false;
dispatchResult.error = result;
}
}
if (customEvent.detail.proceed === true) {
dispatchResult.canceled = false;
} else if (!customEvent.detail.cancelHandler) {
customEvent.detail.cancelHandler =
// customEvent.detail.handlers.at(-1)
customEvent.detail.handlers.slice(-1)[0];
}
return Object.assign(dispatchResult, { event: customEvent });
};
}());
</script>