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

@@ -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]
}
}