Skip to content

Commit

Permalink
perf: use removeEventListener to remove event listeners
Browse files Browse the repository at this point in the history
  • Loading branch information
Alfred-Skyblue committed Aug 16, 2023
1 parent 480579b commit 24374eb
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 112 deletions.
171 changes: 60 additions & 111 deletions packages/runtime-dom/src/directives/vModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,43 +38,10 @@ function onCompositionEnd(e: Event) {

type ModelDirective<T> = ObjectDirective<T & { _assign: AssignerFn }>

const elementMap = new WeakMap<Element, Set<AbortController>>()

/**
* Creates an AbortController associated with the given element.
*
* @param {HTMLElement} el - The element to associate the AbortController with.
* @returns An object containing the signal property of the created AbortController.
*/
function createAbortController(el: Element) {
const controller = new AbortController()
const target = elementMap.get(el)

if (target) {
target.add(controller)
} else {
elementMap.set(el, new Set([controller]))
}

return { signal: controller.signal }
}

/**
* Cleans up the AbortControllers associated with the given element.
*
* @param {HTMLElement} el - The element to clean up the associated AbortControllers.
*/
function cleanupAbortController(el: HTMLElement) {
const target = elementMap.get(el)

if (target) {
target.forEach(controller => controller.abort())
target.clear()
}
}
const elementMap = new WeakMap<Element, Set<Function>>()

function unmounted(el: HTMLElement) {
cleanupAbortController(el)
elementMap.get(el)?.forEach(off => off())
}

// We are exporting the v-model runtime directly as vnode hooks so that it can
Expand All @@ -83,14 +50,13 @@ export const vModelText: ModelDirective<
HTMLInputElement | HTMLTextAreaElement
> = {
created(el, { modifiers: { lazy, trim, number } }, vnode) {
const options = createAbortController(el)
const off = new Set<Function>()
el._assign = getModelAssigner(vnode)
const castToNumber =
number || (vnode.props && vnode.props.type === 'number')
addEventListener(
el,
lazy ? 'change' : 'input',
e => {

off.add(
addEventListener(el, lazy ? 'change' : 'input', e => {
if ((e.target as any).composing) return
let domValue: string | number = el.value
if (trim) {
Expand All @@ -100,28 +66,26 @@ export const vModelText: ModelDirective<
domValue = looseToNumber(domValue)
}
el._assign(domValue)
},
options
})
)

if (trim) {
addEventListener(
el,
'change',
() => {
off.add(
addEventListener(el, 'change', () => {
el.value = el.value.trim()
},
options
})
)
}
if (!lazy) {
addEventListener(el, 'compositionstart', onCompositionStart, options)
addEventListener(el, 'compositionend', onCompositionEnd, options)
off.add(addEventListener(el, 'compositionstart', onCompositionStart))
off.add(addEventListener(el, 'compositionend', onCompositionEnd))
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
addEventListener(el, 'change', onCompositionEnd, options)
off.add(addEventListener(el, 'change', onCompositionEnd))
}
elementMap.set(el, off)
},
// set value on mounted so it's after min/max for type="range"
mounted(el, { value }) {
Expand Down Expand Up @@ -157,40 +121,36 @@ export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
// #4096 array checkboxes need to be deep traversed
deep: true,
created(el, _, vnode) {
const options = createAbortController(el)
el._assign = getModelAssigner(vnode)
addEventListener(
el,
'change',
() => {
const modelValue = (el as any)._modelValue
const elementValue = getValue(el)
const checked = el.checked
const assign = el._assign
if (isArray(modelValue)) {
const index = looseIndexOf(modelValue, elementValue)
const found = index !== -1
if (checked && !found) {
assign(modelValue.concat(elementValue))
} else if (!checked && found) {
const filtered = [...modelValue]
filtered.splice(index, 1)
assign(filtered)
}
} else if (isSet(modelValue)) {
const cloned = new Set(modelValue)
if (checked) {
cloned.add(elementValue)
} else {
cloned.delete(elementValue)
}
assign(cloned)
const off = addEventListener(el, 'change', () => {
const modelValue = (el as any)._modelValue
const elementValue = getValue(el)
const checked = el.checked
const assign = el._assign
if (isArray(modelValue)) {
const index = looseIndexOf(modelValue, elementValue)
const found = index !== -1
if (checked && !found) {
assign(modelValue.concat(elementValue))
} else if (!checked && found) {
const filtered = [...modelValue]
filtered.splice(index, 1)
assign(filtered)
}
} else if (isSet(modelValue)) {
const cloned = new Set(modelValue)
if (checked) {
cloned.add(elementValue)
} else {
assign(getCheckboxValue(el, checked))
cloned.delete(elementValue)
}
},
options
)
assign(cloned)
} else {
assign(getCheckboxValue(el, checked))
}
})

elementMap.set(el, new Set([off]))
},
// set initial checked on mount to wait for true-value/false-value
mounted: setChecked,
Expand Down Expand Up @@ -220,17 +180,12 @@ function setChecked(

export const vModelRadio: ModelDirective<HTMLInputElement> = {
created(el, { value }, vnode) {
const options = createAbortController(el)
el.checked = looseEqual(value, vnode.props!.value)
el._assign = getModelAssigner(vnode)
addEventListener(
el,
'change',
() => {
el._assign(getValue(el))
},
options
)
const off = addEventListener(el, 'change', () => {
el._assign(getValue(el))
})
elementMap.set(el, new Set([off]))
},
beforeUpdate(el, { value, oldValue }, vnode) {
el._assign = getModelAssigner(vnode)
Expand All @@ -245,29 +200,23 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
// <select multiple> value need to be deep traversed
deep: true,
created(el, { value, modifiers: { number } }, vnode) {
const options = createAbortController(el)

const isSetModel = isSet(value)
addEventListener(
el,
'change',
() => {
const selectedVal = Array.prototype.filter
.call(el.options, (o: HTMLOptionElement) => o.selected)
.map((o: HTMLOptionElement) =>
number ? looseToNumber(getValue(o)) : getValue(o)
)
el._assign(
el.multiple
? isSetModel
? new Set(selectedVal)
: selectedVal
: selectedVal[0]
const off = addEventListener(el, 'change', () => {
const selectedVal = Array.prototype.filter
.call(el.options, (o: HTMLOptionElement) => o.selected)
.map((o: HTMLOptionElement) =>
number ? looseToNumber(getValue(o)) : getValue(o)
)
},
options
)
el._assign(
el.multiple
? isSetModel
? new Set(selectedVal)
: selectedVal
: selectedVal[0]
)
})
el._assign = getModelAssigner(vnode)
elementMap.set(el, new Set([off]))
},
// set value in mounted & updated because <select> relies on its children
// <option>s.
Expand Down
3 changes: 2 additions & 1 deletion packages/runtime-dom/src/modules/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ export function addEventListener(
el: Element,
event: string,
handler: EventListener,
options?: AddEventListenerOptions
options?: EventListenerOptions
) {
el.addEventListener(event, handler, options)
return () => removeEventListener(el, event, handler, options)
}

export function removeEventListener(
Expand Down

0 comments on commit 24374eb

Please sign in to comment.