diff --git a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts index d358431d..f23cdf6a 100644 --- a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts +++ b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts @@ -11,7 +11,8 @@ import { createBlock, FunctionalComponent, createCommentVNode, - Fragment + Fragment, + withModifiers } from '@vue/runtime-dom' import { PatchFlags } from '@vue/shared/src' @@ -383,6 +384,45 @@ describe('attribute fallthrough', () => { expect(`Extraneous non-emits event listeners`).toHaveBeenWarned() }) + it('should dedupe same listeners when $attrs is used during render', () => { + const click = jest.fn() + const count = ref(0) + + function inc() { + count.value++ + click() + } + + const Parent = { + render() { + return h(Child, { onClick: inc }) + } + } + + const Child = defineComponent({ + render() { + return h( + 'div', + mergeProps( + { + onClick: withModifiers(() => {}, ['prevent', 'stop']) + }, + this.$attrs + ) + ) + } + }) + + const root = document.createElement('div') + document.body.appendChild(root) + render(h(Parent), root) + + const node = root.children[0] as HTMLElement + node.dispatchEvent(new CustomEvent('click')) + expect(click).toHaveBeenCalledTimes(1) + expect(count.value).toBe(1) + }) + it('should not warn when $attrs is used during render', () => { const Parent = { render() { diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 089147a3..306afbcc 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -791,7 +791,10 @@ export function mergeProps(...args: (Data & VNodeProps)[]) { } else if (isOn(key)) { const existing = ret[key] const incoming = toMerge[key] - if (existing !== incoming) { + if ( + existing !== incoming && + !(isArray(existing) && existing.includes(incoming)) + ) { ret[key] = existing ? [].concat(existing as any, incoming as any) : incoming