fix(v-on): refactor DOM event options modifer handling

fix #1567

Previously multiple `v-on` handlers with different event attach option
modifers (`.once`, `.capture` and `.passive`) are generated as an array
of objects in the form of `[{ handler, options }]` - however, this
makes it pretty complex for `runtime-dom` to properly handle all
possible value permutations, as each handler may need to be attached
with different options.

With this commit, they are now generated as event props with different
keys - e.g. `v-on:click.capture` is now generated as a prop named
`onClick.capture`. This allows them to be patched as separate props
which makes the runtime handling much simpler.
This commit is contained in:
Evan You
2020-07-14 11:48:05 -04:00
parent 9152a89016
commit 380c6792d8
8 changed files with 200 additions and 189 deletions

View File

@@ -57,17 +57,11 @@ describe(`runtime-dom: events patching`, () => {
expect(fn).not.toHaveBeenCalled()
})
it('should support event options', async () => {
it('should support event option modifiers', async () => {
const el = document.createElement('div')
const event = new Event('click')
const fn = jest.fn()
const nextValue = {
handler: fn,
options: {
once: true
}
}
patchProp(el, 'onClick', null, nextValue)
patchProp(el, 'onClick.once.capture', null, fn)
el.dispatchEvent(event)
await timeout()
el.dispatchEvent(event)
@@ -75,39 +69,12 @@ describe(`runtime-dom: events patching`, () => {
expect(fn).toHaveBeenCalledTimes(1)
})
it('should support varying event options', async () => {
const el = document.createElement('div')
const event = new Event('click')
const prevFn = jest.fn()
const nextFn = jest.fn()
const nextValue = {
handler: nextFn,
options: {
once: true
}
}
patchProp(el, 'onClick', null, prevFn)
patchProp(el, 'onClick', prevFn, nextValue)
el.dispatchEvent(event)
await timeout()
el.dispatchEvent(event)
await timeout()
expect(prevFn).not.toHaveBeenCalled()
expect(nextFn).toHaveBeenCalledTimes(1)
})
it('should unassign event handler with options', async () => {
const el = document.createElement('div')
const event = new Event('click')
const fn = jest.fn()
const nextValue = {
handler: fn,
options: {
once: true
}
}
patchProp(el, 'onClick', null, nextValue)
patchProp(el, 'onClick', nextValue, null)
patchProp(el, 'onClick.capture', null, fn)
patchProp(el, 'onClick.capture', fn, null)
el.dispatchEvent(event)
await timeout()
el.dispatchEvent(event)

View File

@@ -1,4 +1,4 @@
import { EMPTY_OBJ, isArray } from '@vue/shared'
import { isArray } from '@vue/shared'
import {
ComponentInternalInstance,
callWithAsyncErrorHandling
@@ -14,12 +14,6 @@ type EventValue = (Function | Function[]) & {
invoker?: Invoker | null
}
type EventValueWithOptions = {
handler: EventValue
options: AddEventListenerOptions
invoker?: Invoker | null
}
// Async edge case fix requires storing an event listener's attach timestamp.
let _getNow: () => number = Date.now
@@ -67,52 +61,43 @@ export function removeEventListener(
export function patchEvent(
el: Element,
rawName: string,
prevValue: EventValueWithOptions | EventValue | null,
nextValue: EventValueWithOptions | EventValue | null,
prevValue: EventValue | null,
nextValue: EventValue | null,
instance: ComponentInternalInstance | null = null
) {
const name = rawName.slice(2).toLowerCase()
const prevOptions = prevValue && 'options' in prevValue && prevValue.options
const nextOptions = nextValue && 'options' in nextValue && nextValue.options
const invoker = prevValue && prevValue.invoker
const value =
nextValue && 'handler' in nextValue ? nextValue.handler : nextValue
if (prevOptions || nextOptions) {
const prev = prevOptions || EMPTY_OBJ
const next = nextOptions || EMPTY_OBJ
if (
prev.capture !== next.capture ||
prev.passive !== next.passive ||
prev.once !== next.once
) {
if (invoker) {
removeEventListener(el, name, invoker, prev)
}
if (nextValue && value) {
const invoker = createInvoker(value, instance)
nextValue.invoker = invoker
addEventListener(el, name, invoker, next)
}
return
if (nextValue && invoker) {
// patch
;(prevValue as EventValue).invoker = null
invoker.value = nextValue
nextValue.invoker = invoker
} else {
const [name, options] = parseName(rawName)
if (nextValue) {
addEventListener(el, name, createInvoker(nextValue, instance), options)
} else if (invoker) {
// remove
removeEventListener(el, name, invoker, options)
}
}
}
if (nextValue && value) {
if (invoker) {
;(prevValue as EventValue).invoker = null
invoker.value = value
nextValue.invoker = invoker
} else {
addEventListener(
el,
name,
createInvoker(value, instance),
nextOptions || void 0
)
}
} else if (invoker) {
removeEventListener(el, name, invoker, prevOptions || void 0)
const optionsModifierRE = /\.(once|passive|capture)\b/g
function parseName(name: string): [string, EventListenerOptions | undefined] {
name = name.slice(2).toLowerCase()
if (optionsModifierRE.test(name)) {
const options: EventListenerOptions = {}
name = name.replace(
optionsModifierRE,
(_, key: keyof EventListenerOptions) => {
options[key] = true
return ''
}
)
return [name, options]
} else {
return [name, undefined]
}
}