Skip to content

Commit

Permalink
refactor: drop event delegation and use simple async edge case fix
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Jan 22, 2019
1 parent 9449dfb commit 360a10f
Show file tree
Hide file tree
Showing 6 changed files with 46 additions and 146 deletions.
7 changes: 1 addition & 6 deletions packages/runtime-core/src/createRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ export interface PatchDataFunction {
export interface RendererOptions {
nodeOps: NodeOps
patchData: PatchDataFunction
teardownVNode?: (vnode: VNode) => void
}

export interface FunctionalHandle {
Expand Down Expand Up @@ -102,8 +101,7 @@ export function createRenderer(options: RendererOptions) {
nextSibling: platformNextSibling,
querySelector: platformQuerySelector
},
patchData: platformPatchData,
teardownVNode
patchData: platformPatchData
} = options

function queueInsertOrAppend(
Expand Down Expand Up @@ -1138,9 +1136,6 @@ export function createRenderer(options: RendererOptions) {
data.vnodeBeforeUnmount(vnode)
}
unmountChildren(children as VNodeChildren, childFlags)
if (teardownVNode !== void 0) {
teardownVNode(vnode)
}
if (isElement && data != null && data.vnodeUnmounted) {
data.vnodeUnmounted(vnode)
}
Expand Down
4 changes: 1 addition & 3 deletions packages/runtime-dom/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { createRenderer, Component } from '@vue/runtime-core'
import { nodeOps } from './nodeOps'
import { patchData } from './patchData'
import { teardownVNode } from './teardownVNode'

const { render: _render } = createRenderer({
nodeOps,
patchData,
teardownVNode
patchData
})

type publicRender = (
Expand Down
155 changes: 40 additions & 115 deletions packages/runtime-dom/src/modules/events.ts
Original file line number Diff line number Diff line change
@@ -1,106 +1,60 @@
const delegateRE = /^(?:click|dblclick|submit|(?:key|mouse|touch|pointer).*)$/
import { isChrome } from '../ua'

type EventValue = Function | Function[]
type TargetRef = { el: Element | Document }
interface Invoker extends Function {
value: EventValue
lastUpdated?: number
}

type EventValue = (Function | Function[]) & {
invoker?: Invoker | null
}

export function patchEvent(
el: Element,
name: string,
prevValue: EventValue | null,
nextValue: EventValue | null
) {
if (delegateRE.test(name) && !__JSDOM__) {
handleDelegatedEvent(el, name, nextValue)
} else {
handleNormalEvent(el, name, prevValue, nextValue)
}
}

const eventCounts: Record<string, number> = {}
const attachedGlobalHandlers: Record<string, Function | null> = {}

export function handleDelegatedEvent(
el: any,
name: string,
value: EventValue | null
) {
const count = eventCounts[name]
let store = el.__events
if (value) {
if (!count) {
attachGlobalHandler(name)
}
if (!store) {
store = el.__events = {}
}
if (!store[name]) {
eventCounts[name]++
}
store[name] = value
} else if (store && store[name]) {
if (--eventCounts[name] === 0) {
removeGlobalHandler(name)
const invoker = prevValue && prevValue.invoker
if (nextValue) {
if (invoker) {
;(prevValue as EventValue).invoker = null
invoker.value = nextValue
nextValue.invoker = invoker
if (isChrome) {
invoker.lastUpdated = performance.now()
}
} else {
el.addEventListener(name, createInvoker(nextValue))
}
store[name] = null
} else if (invoker) {
el.removeEventListener(name, invoker as any)
}
}

function attachGlobalHandler(name: string) {
const handler = (attachedGlobalHandlers[name] = (e: Event) => {
const isClick = e.type === 'click' || e.type === 'dblclick'
if (isClick && (e as MouseEvent).button !== 0) {
e.stopPropagation()
return false
}
e.stopPropagation = stopPropagation
const targetRef: TargetRef = { el: document }
Object.defineProperty(e, 'currentTarget', {
configurable: true,
get() {
return targetRef.el
}
})
dispatchEvent(e, name, isClick, targetRef)
})
document.addEventListener(name, handler)
eventCounts[name] = 0
}

function stopPropagation() {
this.cancelBubble = true
if (!this.immediatePropagationStopped) {
this.stopImmediatePropagation()
function createInvoker(value: any) {
const invoker = ((e: Event) => {
invokeEvents(e, invoker.value, invoker.lastUpdated)
}) as any
invoker.value = value
value.invoker = invoker
if (isChrome) {
invoker.lastUpdated = performance.now()
}
return invoker
}

function dispatchEvent(
e: Event,
name: string,
isClick: boolean,
targetRef: TargetRef
) {
let el = e.target as any
while (el != null) {
// Don't process clicks on disabled elements
if (isClick && el.disabled) {
break
}
const store = el.__events
if (store) {
const value = store[name]
if (value) {
targetRef.el = el
invokeEvents(e, value)
if (e.cancelBubble) {
break
}
}
}
el = el.parentNode
function invokeEvents(e: Event, value: EventValue, lastUpdated: number) {
// async edge case #6566: inner click event triggers patch, event handler
// attached to outer element during patch, and triggered again. This only
// happens in Chrome as it fires microtask ticks between event propagation.
// the solution is simple: we save the timestamp when a handler is attached,
// and the handler would only fire if the event passed to it was fired
// AFTER it was attached.
if (isChrome && e.timeStamp < lastUpdated) {
return
}
}

function invokeEvents(e: Event, value: EventValue) {
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
value[i](e)
Expand All @@ -109,32 +63,3 @@ function invokeEvents(e: Event, value: EventValue) {
value(e)
}
}

function removeGlobalHandler(name: string) {
document.removeEventListener(name, attachedGlobalHandlers[name] as any)
attachedGlobalHandlers[name] = null
}

function handleNormalEvent(el: Element, name: string, prev: any, next: any) {
const invoker = prev && prev.invoker
if (next) {
if (invoker) {
prev.invoker = null
invoker.value = next
next.invoker = invoker
} else {
el.addEventListener(name, createInvoker(next))
}
} else if (invoker) {
el.removeEventListener(name, invoker)
}
}

function createInvoker(value: any) {
const invoker = ((e: Event) => {
invokeEvents(e, invoker.value)
}) as any
invoker.value = value
value.invoker = invoker
return invoker
}
9 changes: 1 addition & 8 deletions packages/runtime-dom/src/modules/style.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { isString } from '@vue/shared'

// style properties that should NOT have "px" added when numeric
const nonNumericRE = /acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i

export function patchStyle(el: any, prev: any, next: any, data: any) {
const { style } = el
if (!next) {
Expand All @@ -11,11 +8,7 @@ export function patchStyle(el: any, prev: any, next: any, data: any) {
style.cssText = next
} else {
for (const key in next) {
let value = next[key]
if (typeof value === 'number' && !nonNumericRE.test(key)) {
value = value + 'px'
}
style[key] = value
style[key] = next[key]
}
if (prev && !isString(prev)) {
for (const key in prev) {
Expand Down
14 changes: 0 additions & 14 deletions packages/runtime-dom/src/teardownVNode.ts

This file was deleted.

3 changes: 3 additions & 0 deletions packages/runtime-dom/src/ua.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const UA = window.navigator.userAgent.toLowerCase()
export const isEdge = UA.indexOf('edge/') > 0
export const isChrome = /chrome\/\d+/.test(UA) && !isEdge

0 comments on commit 360a10f

Please sign in to comment.