我想创建一个私人自定义事件,一个其他人无法调度或侦听的事件。我尝试了这个,但它不起作用:
new CustomEvent(Symbol("validate"));
可以吗?
我知道我可以使用随机字符串作为事件的名称。
来自一些OP和我的评论......
当然,人们可以构建一个系统来封闭/封装除除例如之外的所有内容。实例的 de/register / un/subscribe 方法,可以在其中添加/删除对象,每个对象都有自己的(事件/主题/主题/通道特定)处理函数。调度实例没有公共
方法,但会调用每个注册/订阅对象的特定处理函数。 与此类似的东西... – Peter SeligerdispatchEvent
@PeterSeliger 事件字符串的问题是:它们共享一个全局名称空间。没有什么可以阻止碰撞。 – 天花板
不...如果一个人[私下]将自己的
实例与能够将事件分派给选定(验证和注册)对象的类型的每个实例绑定,那么除了秘密/私下分派实例本身之外,没有人有能力访问此类分派事件及其相关数据。这是因为只有调度实例本身可以访问调度对象。实例本身控制对象的取消/注册,每个对象必须能够处理此类受控分派(并且否则不可访问)事件类型。 – 彼得·塞利格EventTarget
特别是关于...
“事件字符串的问题是:它们共享一个全局名称空间。没有什么可以防止冲突。” – ceving
EventTarget
的事件调度实现了类型安全的 Signals 和 Slot 方法。没有人可以伪造/拦截/欺骗在 addEventListener
的调用时间创建的event-object。此外,在事件目标的
dispatchEvent
时间,仅在该事件目标处注册的事件类型特定处理程序函数(每个指定为创建的事件侦听器 handleEvent
方法)会被注册。将被调用。
按照建议(以及后来演示的),通过
addSubscriber/s
/ removeSubscriber/s
实现的发布-订阅与事件目标的封闭(私有)功能相结合,可以实现事件的隐私和类型安全-调度。
const subscriber_1 = {
uuid: crypto.randomUUID(),
handleTypeA(evt) {
console.log({
eventType: evt.type,
currentTarget: evt.currentTarget,
targetConstructorName: evt.detail.target.constructor.name,
subscriber: subscriber_1,
payload: evt.detail.payload,
});
}
};
const subscriber_2 = {
uuid: crypto.randomUUID(),
handleTypeB(evt) {
console.log({
eventType: evt.type,
currentTarget: evt.currentTarget,
targetConstructorName: evt.detail.target.constructor.name,
subscriber: subscriber_2,
payload: evt.detail.payload,
});
}
};
const subscriber_3 = {
uuid: crypto.randomUUID(),
handleTypeB(evt) {
console.log({
eventType: evt.type,
currentTarget: evt.currentTarget,
targetConstructorName: evt.detail.target.constructor.name,
subscriber: subscriber_3,
payload: evt.detail.payload,
});
},
handleTypeZ(evt) {
console.log({
eventType: evt.type,
currentTarget: evt.currentTarget,
targetConstructorName: evt.detail.target.constructor.name,
subscriber: subscriber_3,
payload: evt.detail.payload,
});
}
};
const sampleInstance = new PrivatlyDispatchingType;
console.log(
'sampleInstance.addSubscribers(subscriber_1, subscriber_2, subscriber_3) ...\n',
'... sampleInstance.aggregatesAndDispatchesSampleData() ...',
);
sampleInstance.addSubscribers(subscriber_1, subscriber_2, subscriber_3);
sampleInstance.aggregatesAndDispatchesSampleData();
console.log(
'sampleInstance.removeSubscribers([subscriber_1, subscriber_3]) ...\n',
'... sampleInstance.aggregatesAndDispatchesSampleData() ...',
);
sampleInstance.removeSubscribers([subscriber_1, subscriber_3]);
sampleInstance.aggregatesAndDispatchesSampleData();;
console.log(
'sampleInstance.removeSubscribers(subscriber_2) ...\n',
'... sampleInstance.aggregatesAndDispatchesSampleData() ...',
);
sampleInstance.removeSubscribers(subscriber_2);
sampleInstance.aggregatesAndDispatchesSampleData();
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
// scope of module ... `PrivatlyDispatchingType`
function isValidSubscriber(value) {
// e.g. implement other custom type check functionality.
return (
(typeof value?.uuid === 'string') &&
(Object
.entries(value)
.filter(([key, value]) =>
key.startsWith('handle') && (typeof value === 'function')
)
.length >= 1)
);
}
function getValidSubscribers(...subscribers) {
return subscribers.flat().filter(isValidSubscriber);
}
const eventTypeLookup = {
handleTypeA: 'private-event-type-A',
handleTypeB: 'private-event-type-B',
handleTypeZ: 'private-event-type-Z',
}
class PrivatlyDispatchingType {
#dispatcher = new EventTarget;
#subscribers = new Map;
/**
* type specific implementation
*/
// ----- ----- ----- ----- -----
addSubscribers(...args) {
getValidSubscribers(...args)
.forEach(subscriber => {
const { uuid } = subscriber;
if (!this.#subscribers.has(uuid)) {
this.#subscribers.set(uuid, subscriber);
Object
.entries(subscriber)
.filter(([key, value]) =>
key.startsWith('handle') && (typeof value === 'function')
)
.forEach(([handlerName, handlerFunction]) =>
this.#dispatcher.addEventListener(
eventTypeLookup[handlerName], handlerFunction
)
);
}
});
}
removeSubscribers(...args) {
getValidSubscribers(...args)
.forEach(subscriber => {
const { uuid } = subscriber;
if (this.#subscribers.has(uuid)) {
this.#subscribers.delete(uuid);
Object
.entries(subscriber)
.filter(([key, value]) =>
key.startsWith('handle') && (typeof value === 'function')
)
.forEach(([handlerName, handlerFunction]) =>
this.#dispatcher.removeEventListener(
eventTypeLookup[handlerName], handlerFunction
)
);
}
});
}
aggregatesAndDispatchesSampleData() {
this.#dispatcher.dispatchEvent(new CustomEvent(
'private-event-type-A', {
detail: {
target: this,
payload: {
foo: 'Foo',
bar: 'Bar',
}
}
})
);
this.#dispatcher.dispatchEvent(new CustomEvent(
'private-event-type-B', {
detail: {
target: this,
payload: {
baz: 'Baz',
biz: 'Biz',
}
}
})
);
this.#dispatcher.dispatchEvent(new CustomEvent(
'private-event-type-Z', {
detail: {
target: this,
payload: {
quick: 'quick',
brown: 'brown',
fox: 'fox',
}
}
})
);
}
}
</script>
代码复用“独家发布行为”
下一个提供的示例代码演示了一种可能的 代码重用方式,它仍然支持 独家发布。
该实现确实通过利用单个
WeakMap
实例来覆盖可重用实现的 ExclusivePublisher
代码的隐私部分来实现这种行为。然而,后一个类不能被想要获得独占发布行为的第三方代码直接实例化。相反, "exclusive-publishing"
模块公开了一个 useExclusivePublishing
方法,该方法可以访问 ExclusivePublisher
实例的 publisher 和 dispatcher 功能。
因此,任何使用
publisher
first的
dispatcher
和 useExclusivePublishing
返回值的第三方代码都必须将两个引用保持为私有,并且 second 需要实现 addSubscribers
和 removeSubscribers
的转发代码,并且此外还需要实现围绕 dispatcher
的 dispatchEvent
方法构建的自己的发布代码。
// import { useExclusivePublishing } from 'exclusive-publishing.js'
class MyExclusivePublishingType {
#publisher;
#dispatcher;
constructor(eventTypes, isSubscriber) {
const { publisher, dispatcher } =
useExclusivePublishing(eventTypes, isSubscriber);
// private property based aggregation.
this.#publisher = publisher;
this.#dispatcher = dispatcher;
}
// forwarding via aggregated private properties.
addSubscribers(...args) {
return this.#publisher.addSubscribers(...args);
}
removeSubscribers(...args) {
return this.#publisher.removeSubscribers(...args);
}
// type-specific prototypal implementation/s.
publishSampleData() {
this.#dispatcher.dispatchEvent(new CustomEvent(
'private-event-type-A', {
detail: {
target: this,
payload: {
foo: 'Foo',
bar: 'Bar',
}
}
})
);
this.#dispatcher.dispatchEvent(new CustomEvent(
'private-event-type-B', {
detail: {
target: this,
payload: {
baz: 'Baz',
biz: 'Biz',
}
}
})
);
this.#dispatcher.dispatchEvent(new CustomEvent(
'private-event-type-Z', {
detail: {
target: this,
payload: {
quick: 'quick',
brown: 'brown',
fox: 'fox',
}
}
})
);
}
}
const publishingType = new MyExclusivePublishingType({
handleTypeA: 'private-event-type-A',
handleTypeB: 'private-event-type-B',
handleTypeZ: 'private-event-type-Z',
});
const subscriber_1 = {
uuid: crypto.randomUUID(),
handleTypeA(evt) {
console.log({
eventType: evt.type,
currentTarget: evt.currentTarget,
targetConstructorName: evt.detail.target.constructor.name,
subscriber: subscriber_1,
payload: evt.detail.payload,
});
}
};
const subscriber_2 = {
uuid: crypto.randomUUID(),
handleTypeB(evt) {
console.log({
eventType: evt.type,
currentTarget: evt.currentTarget,
targetConstructorName: evt.detail.target.constructor.name,
subscriber: subscriber_2,
payload: evt.detail.payload,
});
}
};
const subscriber_3 = {
uuid: crypto.randomUUID(),
handleTypeB(evt) {
console.log({
eventType: evt.type,
currentTarget: evt.currentTarget,
targetConstructorName: evt.detail.target.constructor.name,
subscriber: subscriber_3,
payload: evt.detail.payload,
});
},
handleTypeZ(evt) {
console.log({
eventType: evt.type,
currentTarget: evt.currentTarget,
targetConstructorName: evt.detail.target.constructor.name,
subscriber: subscriber_3,
payload: evt.detail.payload,
});
}
};
console.log(
'publishingType.addSubscribers(subscriber_1, subscriber_2, subscriber_3) ...\n',
'... publishingType.publishSampleData() ...',
);
publishingType.addSubscribers(subscriber_1, subscriber_2, subscriber_3);
publishingType.publishSampleData();
console.log(
'publishingType.removeSubscribers([subscriber_1, subscriber_3]) ...\n',
'... publishingType.publishSampleData() ...',
);
publishingType.removeSubscribers([subscriber_1, subscriber_3]);
publishingType.publishSampleData();;
console.log(
'publishingType.removeSubscribers(subscriber_2) ...\n',
'... publishingType.publishSampleData() ...',
);
publishingType.removeSubscribers(subscriber_2);
publishingType.publishSampleData();
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
// scope of module ... "exclusive-publishing.js"
const publisherRegistry = new WeakMap;
function isFunction(value) {
return (
(typeof value === 'function') &&
(typeof value.call === 'function') &&
(typeof value.apply === 'function')
);
}
function isMinimumSubscriber(value) {
return (
(typeof value?.uuid === 'string') &&
(Object
.entries(value)
.filter(([key, value]) =>
key.startsWith('handle') && isFunction(value)
)
.length >= 1)
);
}
function getSubscribers(isSubscriber, ...subscribers) {
return subscribers.flat().filter(isSubscriber);
}
function getHandlerEntries(subscriber) {
return Object
.entries(subscriber)
.filter(([key, value]) =>
key.startsWith('handle') && isFunction(value)
);
}
function addSubscribers(publisher, ...args) {
const {
eventTypes, dispatcher, subscribers: subscriberRegistry, isSubscriber,
} =
publisherRegistry.get(publisher);
getSubscribers(isSubscriber, ...args)
.forEach(subscriber => {
const { uuid } = subscriber;
if (!subscriberRegistry.has(uuid)) {
subscriberRegistry.set(uuid, subscriber);
getHandlerEntries(subscriber)
.forEach(([handlerName, handlerFunction]) =>
dispatcher.addEventListener(
eventTypes[handlerName], handlerFunction
)
);
}
});
}
function removeSubscribers(publisher, ...args) {
const {
eventTypes, dispatcher, subscribers: subscriberRegistry, isSubscriber,
} =
publisherRegistry.get(publisher);
getSubscribers(isSubscriber, ...args)
.forEach(subscriber => {
const { uuid } = subscriber;
if (subscriberRegistry.has(uuid)) {
subscriberRegistry.delete(uuid);
getHandlerEntries(subscriber)
.forEach(([handlerName, handlerFunction]) =>
dispatcher.removeEventListener(
eventTypes[handlerName], handlerFunction
)
);
}
});
}
class ExclusivePublisher {
constructor(eventTypes = {}, isSubscriber) {
publisherRegistry
.set(this, {
eventTypes,
dispatcher: new EventTarget,
subscribers: new Map,
isSubscriber: isFunction(isSubscriber)
&& (value => isMinimumSubscriber(value) && isSubscriber(value))
|| isMinimumSubscriber,
});
}
// - minimum prototypal implementation/footprint because of fowarding
// which is possible due to the `WeakMap`-based privacy-approach.
addSubscribers(...args) {
return addSubscribers(this, ...args);
}
removeSubscribers(...args) {
return removeSubscribers(this, ...args);
}
}
/*export */function useExclusivePublishing(eventTypes, isSubscriber) {
const publisher = new ExclusivePublisher(eventTypes, isSubscriber);
const { dispatcher } = publisherRegistry.get(publisher);
return {
publisher, dispatcher,
};
}
</script>
通过重写我最近提供的答案之一的示例代码,将再次证明“独家发布行为”的实用性、可重用性和优点...“使用 Node EventEmitter,如何是否可以从服务列表中删除对象,同时取消订阅服务的事件调度?”.
关于刚刚提到的答案的最后一个示例代码...而不是
Bakery
类让扩展 EventTarget
允许每个有权访问面包店实例的人调用其所有 3 个继承方法(add/removeEventListener
/ dispatchEvent
),可以在上面介绍的 Bakery
模块的 useExclusivePublishing
的帮助下实现 'exclusive-publishing'
类。
// import { Bakery } from './bakery.js';
// import { Customer } from './customer.js';
const klugesherz =
new Bakery({ name: 'Pâtisserie Klugesherz', breadPrice: 1.5 });
const hanss =
new Bakery({ name: 'Boulangerie Patisserie Hanss', breadPrice: 2.7 });
const johnRich = new Customer({
name: 'John Rich',
maxPrice: 5,
moneyTotal: 20,
handlePurchaseBread: function ({ detail: { bakery } }) {
this.buyBread(bakery, 3);
},
});
const martinPoor = new Customer({
name: 'Martin Poor',
maxPrice: 3,
moneyTotal: 10,
handlePurchaseBread: function ({ detail: { bakery } }) {
const quantity = (
((bakery.name === 'Boulangerie Patisserie Hanss') && 1) ||
((bakery.name === 'Pâtisserie Klugesherz') && 2) || 0
);
this.buyBread(bakery, quantity);
},
});
klugesherz.addCustomers(johnRich, martinPoor);
hanss.addCustomers(johnRich, martinPoor);
console.log({
bakeries: {
klugesherz: klugesherz.valueOf(),
hanss: hanss.valueOf(),
},
customers: {
johnRich: johnRich.valueOf(),
martinPoor: martinPoor.valueOf(),
},
});
console.log('\n+++ klugesherz.bakeBread(4) +++');
klugesherz.bakeBread(4);
console.log('\n+++ hanss.bakeBread(5) +++');
hanss.bakeBread(5);
console.log('\n+++ klugesherz.bakeBread(4) +++');
klugesherz.bakeBread(4);
console.log('\n+++ hanss.bakeBread(5) +++');
hanss.bakeBread(5);
console.log('\n... remove John Rich from the customer list of Patisserie Hanss.\n\n');
hanss.removeCustomers(johnRich);
console.log('\n+++ klugesherz.bakeBread(4) +++');
klugesherz.bakeBread(4);
console.log('\n+++ hanss.bakeBread(5) +++');
hanss.bakeBread(5);
console.log('\n... remove Martin Poor from the customer list of Patisserie Hanss.');
hanss.removeCustomers(martinPoor);
console.log('... remove John Rich and Martin Poor from the customer list of Pâtisserie Klugesherz.\n\n');
klugesherz.removeCustomers(johnRich, martinPoor);
console.log('+++ klugesherz.bakeBread(4) +++');
klugesherz.bakeBread(4);
console.log('+++ hanss.bakeBread(5) +++');
hanss.bakeBread(5);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
// scope of module ... "exclusive-publishing.js"
const publisherRegistry = new WeakMap;
function isFunction(value) {
return (
(typeof value === 'function') &&
(typeof value.call === 'function') &&
(typeof value.apply === 'function')
);
}
function isMinimumSubscriber(subscriber) {
return (
(typeof subscriber?.uuid === 'string') &&
(Object
.entries(subscriber.valueOf?.() ?? subscriber)
.filter(([key, value]) =>
key.startsWith('handle') && isFunction(value)
)
.length >= 1)
);
}
function getSubscribers(isSubscriber, ...subscribers) {
return subscribers.flat().filter(isSubscriber);
}
function getHandlerEntries(subscriber) {
return Object
.entries(subscriber.valueOf?.() ?? subscriber)
.filter(([key, value]) =>
key.startsWith('handle') && isFunction(value)
);
}
function addSubscribers(publisher, ...args) {
const {
eventTypes, dispatcher, subscribers: subscriberRegistry, isSubscriber,
} =
publisherRegistry.get(publisher);
getSubscribers(isSubscriber, ...args)
.forEach(subscriber => {
const { uuid } = subscriber;
if (!subscriberRegistry.has(uuid)) {
subscriberRegistry.set(uuid, subscriber);
getHandlerEntries(subscriber)
.forEach(([handlerName, handlerFunction]) =>
dispatcher.addEventListener(
eventTypes[handlerName], handlerFunction
)
);
}
});
}
function removeSubscribers(publisher, ...args) {
const {
eventTypes, dispatcher, subscribers: subscriberRegistry, isSubscriber,
} =
publisherRegistry.get(publisher);
getSubscribers(isSubscriber, ...args)
.forEach(subscriber => {
const { uuid } = subscriber;
if (subscriberRegistry.has(uuid)) {
subscriberRegistry.delete(uuid);
getHandlerEntries(subscriber)
.forEach(([handlerName, handlerFunction]) =>
dispatcher.removeEventListener(
eventTypes[handlerName], handlerFunction
)
);
}
});
}
class ExclusivePublisher {
constructor(eventTypes = {}, isSubscriber) {
publisherRegistry
.set(this, {
eventTypes,
dispatcher: new EventTarget,
subscribers: new Map,
isSubscriber: isFunction(isSubscriber)
&& (value => isMinimumSubscriber(value) && isSubscriber(value))
|| isMinimumSubscriber,
});
}
// - minimum prototypal implementation/footprint because of fowarding
// which is possible due to the `WeakMap`-based privacy-approach.
addSubscribers(...args) {
return addSubscribers(this, ...args);
}
removeSubscribers(...args) {
return removeSubscribers(this, ...args);
}
}
/*export */function useExclusivePublishing(eventTypes, isSubscriber) {
const publisher = new ExclusivePublisher(eventTypes, isSubscriber);
const { dispatcher, subscribers } = publisherRegistry.get(publisher);
return {
publisher, dispatcher, subscribers,
};
}
</script>
<script>
// scope of module ... "customer.js"
/*export */class Customer {
#uuid;
#name;
#maxPrice;
#moneyTotal;
#handlePurchaseBread;
constructor({ name, maxPrice, moneyTotal, handlePurchaseBread }) {
this.#uuid = crypto.randomUUID();
this.#name = name;
this.#maxPrice = maxPrice;
this.#moneyTotal = moneyTotal;
this.#handlePurchaseBread = (typeof handlePurchaseBread === 'function')
&& handlePurchaseBread.bind(this)
|| (({ currentTarget: bakery }) => { this.buyBread(bakery, 1); });
}
get uuid() {
return this.#uuid;
}
get name() {
return this.#name;
}
get maxPrice() {
return this.#maxPrice;
}
get moneyTotal() {
return this.#moneyTotal;
}
get handlePurchaseBread() {
return this.#handlePurchaseBread;
}
buyBread(bakery, quantity = 1) {
const { approved, reason } = bakery.sellBread(this, quantity);
if (approved === true) {
this.#moneyTotal = this.moneyTotal - (bakery.breadPrice * quantity);
console.log(
`Customer ${ this.name } bought ${ quantity } piece/s of bread for a total of ${ (bakery.breadPrice * quantity).toFixed(2) } at ${ bakery.name }.`
);
} else if (typeof reason === 'string') {
console.log('Buying a bread did fail, due to ...', reason);
}
}
valueOf() {
const { uuid, name, maxPrice, moneyTotal, handlePurchaseBread } = this;
return { uuid, name, maxPrice, moneyTotal, handlePurchaseBread };
}
}
/*export */function isCustomer(value) {
return ((value instanceof Customer) || (
Object
.keys(value.valueOf())
.sort()
.join('_') === 'handlePurchaseBread_maxPrice_moneyTotal_name_uuid'
));
}
</script>
<script>
// scope of module ... "bakery.js"
// import { useExclusivePublishing } from './exclusive-publishing.js';
// import { isCustomer } from './customer.js';
/*export */class Bakery {
#name;
#breadPrice;
#breadCount;
#moneyTotal;
#customers;
#publisher;
#dispatcher;
constructor({ name, breadPrice }) {
this.#name = name;
this.#breadPrice = breadPrice;
this.#breadCount = 0;
this.#moneyTotal = 0;
const { publisher, dispatcher, subscribers: customers } =
useExclusivePublishing({ handlePurchaseBread: 'bread-baked' }, isCustomer);
this.#customers = customers;
this.#publisher = publisher;
this.#dispatcher = dispatcher;
}
get name() {
return this.#name;
}
get breadPrice() {
return this.#breadPrice;
}
get breadCount() {
return this.#breadCount;
}
get moneyTotal() {
return this.#moneyTotal;
}
get customers() {
return [...this.#customers.values()];
}
addCustomers(...args) {
return this.#publisher.addSubscribers(...args);
}
removeCustomers(...args) {
return this.#publisher.removeSubscribers(...args);
}
bakeBread(quantity = 10) {
this.#breadCount = this.#breadCount + quantity;
this.#dispatcher.dispatchEvent(
new CustomEvent('bread-baked', { detail: { bakery: this, quantity } })
);
}
sellBread(customer, quantity = 1) {
const transaction = { approved: false };
if (quantity >= 1) {
if (this.breadCount >= quantity) {
const isWithinPriceLimit = this.breadPrice <= customer.maxPrice;
const canEffortPurchase = (this.breadPrice * quantity) <= customer.moneyTotal;
if (isWithinPriceLimit) {
if (canEffortPurchase) {
this.#breadCount = this.breadCount - quantity;
transaction.approved = true;
} else {
transaction.reason =
`Customer ${ customer.name } doesn't have enough money for buying a bread at ${ this.name }.`;
}
} else {
transaction.reason =
`Customer ${ customer.name } does have a price limit which just did exceed at ${ this.name }.`;
}
} else {
transaction.reason =
`The ${ this.name } bakery is too low on bread stock in order to fulfill ${ customer.name }'s order.`;
}
} else {
transaction.reason =
`Customer ${ customer.name } did not provide a valid quantity for purchasing bread at ${ this.name }.`;
}
return transaction;
}
valueOf() {
const { name, breadPrice, breadCount, moneyTotal, customers } = this;
return {
name, breadPrice, breadCount, moneyTotal,
customers: customers.map(customer => customer.valueOf()),
};
}
}
</script>
这似乎是可以达到的最大值。
class ObscureEvent {
#name;
#event;
constructor (options) {
this.#name = crypto.randomUUID();
this.#event = new CustomEvent(this.#name, options); }
listen(target, handler) {
target.addEventListener(this.#name, handler); }
dispatch(target) {
target.dispatchEvent(this.#event); } }
const validate = new ObscureEvent({ bubbles: true });