Skip to content

Commit f0c6159

Browse files
committed
perf(events): avoid arguments/forEach allocation in event buses
Internal and External event bus _dispatch/_chain use rest params and indexed loops instead of Array.from(arguments) plus forEach closures. _chain reuses a single args array across subscribers and preserves stock semantics (an array return is passed as one argument; an empty subscriber list returns the fallback). Adds InternalEventBus._chain unit tests.
1 parent 9539446 commit f0c6159

3 files changed

Lines changed: 102 additions & 66 deletions

File tree

src/js/core/tools/ExternalEventBus.js

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -72,34 +72,26 @@ export default class ExternalEventBus {
7272
}
7373
}
7474

75-
_dispatch(){
76-
var args = Array.from(arguments),
77-
key = args.shift(),
78-
result;
79-
80-
if(this.events[key]){
81-
this.events[key].forEach((callback, i) => {
82-
let callResult = callback.apply(this.table, args);
83-
84-
if(!i){
85-
result = callResult;
86-
}
87-
});
75+
_dispatch(key, ...args){
76+
const subs = this.events[key];
77+
const len = subs?.length;
78+
if(len){
79+
const result = subs[0].apply(this.table, args);
80+
81+
for(let i = 1; i < len; i++){
82+
subs[i].apply(this.table, args);
83+
}
84+
85+
return result;
8886
}
89-
90-
return result;
9187
}
9288

93-
_debugDispatch(){
94-
var args = Array.from(arguments),
95-
key = args[0];
96-
97-
args[0] = "ExternalEvent:" + args[0];
98-
89+
_debugDispatch(key, ...args){
9990
if(this.debug === true || this.debug.includes(key)){
100-
console.log(...args);
91+
const debugArgs = ["ExternalEvent:" + key, ...args];
92+
console.log(...debugArgs);
10193
}
10294

103-
return this._dispatch(...arguments);
95+
return this._dispatch(key, ...args);
10496
}
105-
}
97+
}

src/js/core/tools/InternalEventBus.js

Lines changed: 35 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -65,21 +65,28 @@ export default class InternalEventBus {
6565
}
6666

6767
_chain(key, args, initialValue, fallback){
68-
var value = initialValue;
68+
const subs = this.events[key];
6969

70-
if(!Array.isArray(args)){
71-
args = [args];
72-
}
70+
if(subs && subs.length){
71+
if(!Array.isArray(args)){
72+
args = [args];
73+
}
7374

74-
if(this.subscribed(key)){
75-
this.events[key].forEach((subscriber, i) => {
76-
value = subscriber.callback.apply(this, args.concat([value]));
77-
});
75+
//reuse a single args array across subscribers, mutating only the trailing
76+
//accumulator slot, instead of allocating a fresh concat per subscriber
77+
const callArgs = args.slice();
78+
const valueIndex = callArgs.length;
79+
let value = initialValue;
80+
81+
for(let i = 0; i < subs.length; i++){
82+
callArgs[valueIndex] = value;
83+
value = subs[i].callback.apply(this, callArgs);
84+
}
7885

7986
return value;
80-
}else{
81-
return typeof fallback === "function" ? fallback() : fallback;
8287
}
88+
89+
return typeof fallback === "function" ? fallback() : fallback;
8390
}
8491

8592
_confirm(key, args){
@@ -110,53 +117,39 @@ export default class InternalEventBus {
110117
}
111118
}
112119

113-
_dispatch(){
114-
var args = Array.from(arguments),
115-
key = args.shift();
116-
117-
if(this.events[key]){
118-
this.events[key].forEach((subscriber) => {
120+
_dispatch(key, ...args){
121+
const subs = this.events[key];
122+
if(subs){
123+
for(const subscriber of subs){
119124
subscriber.callback.apply(this, args);
120-
});
125+
}
121126
}
122127
}
123128

124-
_debugDispatch(){
125-
var args = Array.from(arguments),
126-
key = args[0];
127-
128-
args[0] = "InternalEvent:" + key;
129-
129+
_debugDispatch(key, ...args){
130130
if(this.debug === true || this.debug.includes(key)){
131-
console.log(...args);
131+
const debugArgs = ["InternalEvent:" + key, ...args];
132+
console.log(...debugArgs);
132133
}
133134

134-
return this._dispatch(...arguments);
135+
return this._dispatch(key, ...args);
135136
}
136137

137-
_debugChain(){
138-
var args = Array.from(arguments),
139-
key = args[0];
140-
141-
args[0] = "InternalEvent:" + key;
142-
138+
_debugChain(key, args, initialValue, fallback){
143139
if(this.debug === true || this.debug.includes(key)){
144-
console.log(...args);
140+
const debugArgs = ["InternalEvent:" + key, args, initialValue, fallback];
141+
console.log(...debugArgs);
145142
}
146143

147-
return this._chain(...arguments);
144+
return this._chain(key, args, initialValue, fallback);
148145
}
149146

150-
_debugConfirm(){
151-
var args = Array.from(arguments),
152-
key = args[0];
153-
154-
args[0] = "InternalEvent:" + key;
155-
147+
_debugConfirm(key, args){
156148
if(this.debug === true || this.debug.includes(key)){
157-
console.log(...args);
149+
const debugArgs = ["InternalEvent:" + key, args];
150+
console.log(...debugArgs);
158151
}
159152

160-
return this._confirm(...arguments);
153+
return this._confirm(key, args);
161154
}
162-
}
155+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import InternalEventBus from "../../../../src/js/core/tools/InternalEventBus";
2+
3+
// Locks the _chain semantics the reuse-args fast path must preserve.
4+
5+
describe("InternalEventBus._chain", () => {
6+
test("threads the accumulator through subscribers in order", () => {
7+
const bus = new InternalEventBus(false);
8+
bus.subscribe("k", (_arg, value) => value + 1);
9+
bus.subscribe("k", (_arg, value) => value * 10);
10+
// priority equal -> insertion order: (0+1)=1 then 1*10=10
11+
expect(bus.chain("k", ["x"], 0, false)).toBe(10);
12+
});
13+
14+
test("passes the leading args before the accumulator value", () => {
15+
const bus = new InternalEventBus(false);
16+
bus.subscribe("k", (a, b, value) => `${a}|${b}|${value}`);
17+
expect(bus.chain("k", ["p", "q"], "init", false)).toBe("p|q|init");
18+
});
19+
20+
test("non-array args is wrapped", () => {
21+
const bus = new InternalEventBus(false);
22+
bus.subscribe("k", (a, value) => `${a}:${value}`);
23+
expect(bus.chain("k", "solo", "v", false)).toBe("solo:v");
24+
});
25+
26+
test("an array return value is forwarded as a SINGLE argument", () => {
27+
const bus = new InternalEventBus(false);
28+
bus.subscribe("k", () => [1, 2, 3]);
29+
bus.subscribe("k", (_arg, value) => (Array.isArray(value) ? value.length : "spread"));
30+
// must be 3 (received [1,2,3] as one arg), not "spread"/NaN from a spread array
31+
expect(bus.chain("k", ["x"], 0, false)).toBe(3);
32+
});
33+
34+
test("no subscribers returns the fallback value", () => {
35+
const bus = new InternalEventBus(false);
36+
expect(bus.chain("missing", ["x"], "init", "fallbackValue")).toBe("fallbackValue");
37+
});
38+
39+
test("no subscribers invokes a function fallback", () => {
40+
const bus = new InternalEventBus(false);
41+
expect(bus.chain("missing", ["x"], "init", () => "fromFn")).toBe("fromFn");
42+
});
43+
44+
test("a key whose subscribers were all removed falls back (not the initial value)", () => {
45+
const bus = new InternalEventBus(false);
46+
const cb = (_arg, value) => value;
47+
bus.subscribe("k", cb);
48+
bus.unsubscribe("k", cb);
49+
expect(bus.chain("k", ["x"], "init", "fallbackValue")).toBe("fallbackValue");
50+
});
51+
});

0 commit comments

Comments
 (0)