Skip to content

Commit cc5d253

Browse files
committed
perf(runtime-dom): avoid event handler array allocations
1 parent 1ce598e commit cc5d253

3 files changed

Lines changed: 79 additions & 24 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { bench, describe } from 'vitest'
2+
import { patchProp } from '../src/patchProp'
3+
4+
describe('runtime-dom events', () => {
5+
bench('dispatch click with single handler', () => {
6+
const el = document.createElement('button')
7+
let count = 0
8+
patchProp(el, 'onClick', null, () => count++)
9+
el.dispatchEvent(new Event('click'))
10+
})
11+
12+
bench('dispatch click with multiple handlers', () => {
13+
const el = document.createElement('button')
14+
let count = 0
15+
patchProp(el, 'onClick', null, [
16+
() => count++,
17+
() => count++,
18+
() => count++,
19+
() => count++,
20+
])
21+
el.dispatchEvent(new Event('click'))
22+
})
23+
})

packages/runtime-dom/__tests__/patchProps.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,33 @@ describe('runtime-dom: props patching', () => {
1919
expect(el.getAttribute('id')).toBe(null)
2020
})
2121

22+
test('invokes multiple event handlers without array map indirection', () => {
23+
const el = document.createElement('div')
24+
const fn1 = vi.fn()
25+
const fn2 = vi.fn()
26+
const handlers = [fn1, fn2]
27+
const mapSpy = vi.spyOn(handlers, 'map')
28+
29+
patchProp(el, 'onClick', null, handlers)
30+
el.dispatchEvent(new Event('click'))
31+
32+
expect(fn1).toHaveBeenCalledTimes(1)
33+
expect(fn2).toHaveBeenCalledTimes(1)
34+
expect(mapSpy).not.toHaveBeenCalled()
35+
})
36+
37+
test('stops invoking later event handlers after stopImmediatePropagation', () => {
38+
const el = document.createElement('div')
39+
const fn1 = vi.fn((e: Event) => e.stopImmediatePropagation())
40+
const fn2 = vi.fn()
41+
42+
patchProp(el, 'onClick', null, [fn1, fn2])
43+
el.dispatchEvent(new Event('click'))
44+
45+
expect(fn1).toHaveBeenCalledTimes(1)
46+
expect(fn2).not.toHaveBeenCalled()
47+
})
48+
2249
test('value', () => {
2350
const el = document.createElement('input')
2451
patchProp(el, 'value', null, 'foo')

packages/runtime-dom/src/modules/events.ts

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,35 @@ function createInvoker(
112112
} else if (e._vts <= invoker.attached) {
113113
return
114114
}
115-
callWithAsyncErrorHandling(
116-
patchStopImmediatePropagation(e, invoker.value),
117-
instance,
118-
ErrorCodes.NATIVE_EVENT_HANDLER,
119-
[e],
120-
)
115+
const value = invoker.value
116+
if (isArray(value)) {
117+
const originalStop = e.stopImmediatePropagation
118+
e.stopImmediatePropagation = () => {
119+
originalStop.call(e)
120+
;(e as any)._stopped = true
121+
}
122+
for (let i = 0; i < value.length; i++) {
123+
if ((e as any)._stopped) {
124+
break
125+
}
126+
const handler = value[i]
127+
if (handler) {
128+
callWithAsyncErrorHandling(
129+
handler,
130+
instance,
131+
ErrorCodes.NATIVE_EVENT_HANDLER,
132+
[e],
133+
)
134+
}
135+
}
136+
} else {
137+
callWithAsyncErrorHandling(
138+
value,
139+
instance,
140+
ErrorCodes.NATIVE_EVENT_HANDLER,
141+
[e],
142+
)
143+
}
121144
}
122145
invoker.value = initialValue
123146
invoker.attached = getNow()
@@ -134,21 +157,3 @@ function sanitizeEventValue(value: unknown, propName: string): EventValue {
134157
)
135158
return NOOP
136159
}
137-
138-
function patchStopImmediatePropagation(
139-
e: Event,
140-
value: EventValue,
141-
): EventValue {
142-
if (isArray(value)) {
143-
const originalStop = e.stopImmediatePropagation
144-
e.stopImmediatePropagation = () => {
145-
originalStop.call(e)
146-
;(e as any)._stopped = true
147-
}
148-
return (value as Function[]).map(
149-
fn => (e: Event) => !(e as any)._stopped && fn && fn(e),
150-
)
151-
} else {
152-
return value
153-
}
154-
}

0 commit comments

Comments
 (0)