Skip to content

Commit fd269d6

Browse files
layershifterlevithomason
authored andcommitted
refactor(eventStack): make eventStack immutable (Semantic-Org#2837)
* refactor(eventStack): make eventStack immutable * docs(EventStack): add README Signed-off-by: Oleksandr Fediashov <[email protected]> * docs(EventStack): update readme
1 parent 5ebeedf commit fd269d6

13 files changed

+697
-232
lines changed

src/lib/eventStack/EventPool.js

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import EventSet from './EventSet'
2+
3+
export default class EventPool {
4+
/**
5+
* @param {String} poolName
6+
* @param {String} eventType
7+
* @param {Function[]} eventHandlers
8+
* @return {EventPool}
9+
*/
10+
static createByType = (poolName, eventType, eventHandlers) => {
11+
const handlerSets = new Map()
12+
handlerSets.set(eventType, new EventSet(eventHandlers))
13+
14+
return new EventPool(poolName, handlerSets)
15+
}
16+
17+
/**
18+
* @param {String} poolName
19+
* @param {Map<String,EventSet>} handlerSets
20+
*/
21+
constructor(poolName, handlerSets) {
22+
/** @private */
23+
this.handlerSets = handlerSets
24+
/** @private */
25+
this.poolName = poolName
26+
}
27+
28+
/**
29+
* @param {String} eventType
30+
* @param {Function[]} eventHandlers
31+
* @return {EventPool}
32+
*/
33+
addHandlers(eventType, eventHandlers) {
34+
const handlerSets = new Map(this.handlerSets)
35+
36+
if (handlerSets.has(eventType)) {
37+
handlerSets.set(eventType, handlerSets.get(eventType).addHandlers(eventHandlers))
38+
} else {
39+
handlerSets.set(eventType, new EventSet(eventHandlers))
40+
}
41+
42+
return new EventPool(this.poolName, handlerSets)
43+
}
44+
45+
/**
46+
* @param {String} eventType
47+
* @param {Event} event
48+
*/
49+
dispatchEvent(eventType, event) {
50+
const handlerSet = this.handlerSets.get(eventType)
51+
52+
if (handlerSet) handlerSet.dispatchEvent(event, this.poolName === 'default')
53+
}
54+
55+
/**
56+
* @param {String} eventType
57+
*/
58+
hasHandlers(eventType) {
59+
const handlerSet = this.handlerSets.get(eventType)
60+
61+
if (handlerSet) return handlerSet.hasHandlers()
62+
return false
63+
}
64+
65+
/**
66+
* @param {String} eventType
67+
* @param {Function[]} eventHandlers
68+
* @return {EventPool}
69+
*/
70+
removeHandlers(eventType, eventHandlers) {
71+
const handlerSets = new Map(this.handlerSets)
72+
73+
if (!handlerSets.has(eventType)) {
74+
return new EventPool(this.poolName, handlerSets)
75+
}
76+
77+
const handlerSet = handlerSets.get(eventType).removeHandlers(eventHandlers)
78+
79+
if (handlerSet.hasHandlers()) {
80+
handlerSets.set(eventType, handlerSet)
81+
} else {
82+
handlerSets.delete(eventType)
83+
}
84+
85+
return new EventPool(this.poolName, handlerSets)
86+
}
87+
}

src/lib/eventStack/EventSet.js

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
export default class EventSet {
2+
/**
3+
* @param {Function[]} eventHandlers
4+
*/
5+
constructor(eventHandlers) {
6+
/** @private {Set<Function>} handlers */
7+
this.handlers = new Set(eventHandlers)
8+
}
9+
10+
/**
11+
* @param {Function[]} eventHandlers
12+
* @return {EventSet}
13+
*/
14+
addHandlers(eventHandlers) {
15+
const handlerSet = new Set(this.handlers)
16+
17+
eventHandlers.forEach((eventHandler) => {
18+
// Heads up!
19+
// We should delete a handler from the set, otherwise it will be not the last element in the
20+
// set.
21+
handlerSet.delete(eventHandler)
22+
handlerSet.add(eventHandler)
23+
})
24+
25+
return new EventSet(handlerSet)
26+
}
27+
28+
/**
29+
* @param {Event} event
30+
* @param {Boolean} dispatchAll
31+
*/
32+
dispatchEvent(event, dispatchAll) {
33+
if (dispatchAll) {
34+
this.handlers.forEach((handler) => {
35+
handler(event)
36+
})
37+
return
38+
}
39+
40+
const recentHandler = [...this.handlers].pop()
41+
42+
recentHandler(event)
43+
}
44+
45+
/**
46+
* @return {Boolean}
47+
*/
48+
hasHandlers() {
49+
return this.handlers.size > 0
50+
}
51+
52+
/**
53+
* @param {Function[]} eventHandlers
54+
* @return {EventSet}
55+
*/
56+
removeHandlers(eventHandlers) {
57+
const handlerSet = new Set(this.handlers)
58+
59+
eventHandlers.forEach((eventHandler) => {
60+
handlerSet.delete(eventHandler)
61+
})
62+
63+
return new EventSet(handlerSet)
64+
}
65+
}

src/lib/eventStack/EventStack.js

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import isBrowser from '../isBrowser'
2+
import EventTarget from './EventTarget'
3+
import normalizeHandlers from './normalizeHandlers'
4+
import normalizeTarget from './normalizeTarget'
5+
6+
export default class EventStack {
7+
/** @private {Map<String,EventTarget> */
8+
targets = new Map()
9+
10+
/**
11+
* @param {String} eventName
12+
* @param {Function|Function[]} eventHandlers
13+
* @param {Object} [options]
14+
* @param {*} [options.target]
15+
* @param {String} [options.pool]
16+
*/
17+
sub(eventName, eventHandlers, options = {}) {
18+
if (!isBrowser()) return
19+
20+
const { target = document, pool = 'default' } = options
21+
const eventTarget = this.getTarget(target)
22+
23+
eventTarget.addHandlers(pool, eventName, normalizeHandlers(eventHandlers))
24+
}
25+
26+
/**
27+
* @param {String} eventName
28+
* @param {Function|Function[]} eventHandlers
29+
* @param {Object} [options]
30+
* @param {*} [options.target]
31+
* @param {String} [options.pool]
32+
*/
33+
unsub(eventName, eventHandlers, options = {}) {
34+
if (!isBrowser()) return
35+
36+
const { target = document, pool = 'default' } = options
37+
const eventTarget = this.getTarget(target, false)
38+
39+
if (eventTarget) {
40+
eventTarget.removeHandlers(pool, eventName, normalizeHandlers(eventHandlers))
41+
if (!eventTarget.hasHandlers()) this.removeTarget(target)
42+
}
43+
}
44+
45+
/**
46+
* @private
47+
* @param {*} target
48+
* @param {Boolean} [autoCreate]
49+
* @return {EventTarget}
50+
*/
51+
getTarget = (target, autoCreate = true) => {
52+
const normalized = normalizeTarget(target)
53+
54+
if (this.targets.has(normalized)) return this.targets.get(normalized)
55+
if (!autoCreate) return null
56+
57+
const eventTarget = new EventTarget(normalized)
58+
this.targets.set(normalized, eventTarget)
59+
60+
return eventTarget
61+
}
62+
63+
/**
64+
* @private
65+
* @param {*} target
66+
*/
67+
removeTarget = (target) => {
68+
this.targets.delete(normalizeTarget(target))
69+
}
70+
}

src/lib/eventStack/EventTarget.js

+76-59
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,97 @@
1-
import _ from 'lodash'
1+
import EventPool from './EventPool'
22

33
export default class EventTarget {
4-
_handlers = {}
5-
_pools = {}
6-
4+
/** @private {Map<String,Function>} */
5+
handlers = new Map()
6+
/** @private {Map<String,EventPool>} */
7+
pools = new Map()
8+
9+
/**
10+
* @param {HTMLElement} target
11+
*/
712
constructor(target) {
13+
/** @private */
814
this.target = target
915
}
1016

11-
// ------------------------------------
12-
// Utils
13-
// ------------------------------------
14-
15-
_emit = name => (event) => {
16-
_.forEach(this._pools, (pool, poolName) => {
17-
const { [name]: handlers } = pool
17+
/**
18+
* @param {String} poolName
19+
* @param {String} eventType
20+
* @param {Function[]} eventHandlers
21+
*/
22+
addHandlers(poolName, eventType, eventHandlers) {
23+
this.removeTargetHandler(eventType)
24+
25+
if (!this.pools.has(poolName)) {
26+
this.pools.set(poolName, EventPool.createByType(poolName, eventType, eventHandlers))
27+
} else {
28+
this.pools.set(poolName, this.pools.get(poolName).addHandlers(eventType, eventHandlers))
29+
}
1830

19-
if (!handlers) return
20-
if (poolName === 'default') {
21-
_.forEach(handlers, handler => handler(event))
22-
return
23-
}
24-
_.last(handlers)(event)
25-
})
31+
this.addTargetHandler(eventType)
2632
}
2733

28-
_normalize = handlers => (_.isArray(handlers) ? handlers : [handlers])
29-
30-
// ------------------------------------
31-
// Listeners handling
32-
// ------------------------------------
33-
34-
_listen = (name) => {
35-
if (_.has(this._handlers, name)) return
36-
const handler = this._emit(name)
37-
38-
this.target.addEventListener(name, handler)
39-
this._handlers[name] = handler
34+
/**
35+
* @return {Boolean}
36+
*/
37+
hasHandlers() {
38+
return this.handlers.size > 0
4039
}
4140

42-
_unlisten = (name) => {
43-
if (_.some(this._pools, name)) return
44-
const { [name]: handler } = this._handlers
41+
/**
42+
* @param {String} poolName
43+
* @param {String} eventType
44+
* @param {Function[]} eventHandlers
45+
*/
46+
removeHandlers(poolName, eventType, eventHandlers) {
47+
const pool = this.pools.get(poolName)
48+
49+
if (pool) {
50+
const newPool = pool.removeHandlers(eventType, eventHandlers)
51+
52+
if (newPool.hasHandlers(eventType)) {
53+
this.removeTargetHandler(eventType)
54+
this.pools.set(poolName, newPool)
55+
} else {
56+
this.removeTargetHandler(eventType)
57+
this.pools.delete(poolName)
58+
}
4559

46-
this.target.removeEventListener(name, handler)
47-
delete this._handlers[name]
60+
if (this.pools.size > 0) this.addTargetHandler(eventType)
61+
}
4862
}
4963

50-
// ------------------------------------
51-
// Pub/sub
52-
// ------------------------------------
53-
54-
empty = () => _.isEmpty(this._handlers)
64+
/**
65+
* @private
66+
* @param {String} eventType
67+
* @param {Map<String,EventPool>} eventPools
68+
* @return {Function}
69+
*/
70+
createEmitter = (eventType, eventPools) => (event) => {
71+
eventPools.forEach((pool) => {
72+
pool.dispatchEvent(eventType, event)
73+
})
74+
}
5575

56-
sub = (name, handlers, pool = 'default') => {
57-
const events = _.uniq([
58-
..._.get(this._pools, `${pool}.${name}`, []),
59-
...this._normalize(handlers),
60-
])
76+
/**
77+
* @private
78+
* @param {String} eventType
79+
*/
80+
addTargetHandler(eventType) {
81+
const handler = this.createEmitter(eventType, this.pools)
6182

62-
this._listen(name)
63-
_.set(this._pools, `${pool}.${name}`, events)
83+
this.handlers.set(eventType, handler)
84+
this.target.addEventListener(eventType, handler)
6485
}
6586

66-
unsub = (name, handlers, pool = 'default') => {
67-
const events = _.without(
68-
_.get(this._pools, `${pool}.${name}`, []),
69-
...this._normalize(handlers),
70-
)
71-
72-
if (events.length > 0) {
73-
_.set(this._pools, `${pool}.${name}`, events)
74-
return
87+
/**
88+
* @private
89+
* @param {String} eventType
90+
*/
91+
removeTargetHandler(eventType) {
92+
if (this.handlers.has(eventType)) {
93+
this.target.removeEventListener(eventType, this.handlers.get(eventType))
94+
this.handlers.delete(eventType)
7595
}
76-
77-
_.set(this._pools, `${pool}.${name}`, undefined)
78-
this._unlisten(name)
7996
}
8097
}

0 commit comments

Comments
 (0)