From e8be9638869cad4496ae56e738718261dfaf2053 Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 5 Sep 2022 17:23:34 +0800 Subject: [PATCH] Match-id-67aa6176d68afaa1fc27e819030181c8f9d889cb --- libs/horizon/src/dom/DOMExternal.ts | 9 +++-- libs/horizon/src/dom/DOMOperator.ts | 4 -- .../DOMPropertiesHandler.ts | 2 +- libs/horizon/src/event/EventBinding.ts | 28 +++++--------- libs/horizon/src/renderer/RootStack.ts | 17 +++++++++ libs/horizon/src/renderer/TreeBuilder.ts | 27 ++++++-------- libs/horizon/src/renderer/render/DomPortal.ts | 8 ++-- libs/horizon/src/renderer/vnode/VNode.ts | 3 +- .../ComponentTest/PortalComponent.test.js | 37 ++++++++++++++++++- 9 files changed, 84 insertions(+), 51 deletions(-) create mode 100644 libs/horizon/src/renderer/RootStack.ts diff --git a/libs/horizon/src/dom/DOMExternal.ts b/libs/horizon/src/dom/DOMExternal.ts index 6b82889f..f3054261 100644 --- a/libs/horizon/src/dom/DOMExternal.ts +++ b/libs/horizon/src/dom/DOMExternal.ts @@ -71,13 +71,14 @@ function findDOMNode(domOrEle?: Element): null | Element | Text { // 情况根节点监听器 function removeRootEventLister(container: Container) { - const root = container._treeRoot; - if (root) { - Object.keys(root.delegatedNativeEvents).forEach(event => { - const listener = root.delegatedNativeEvents[event]; + const events = (container._treeRoot as any).$EV; + if (events) { + Object.keys(events).forEach(event => { + const listener = events[event]; if (listener) { container.removeEventListener(event, listener); + events[event] = null; } }); } diff --git a/libs/horizon/src/dom/DOMOperator.ts b/libs/horizon/src/dom/DOMOperator.ts index 85b35da4..4e6b9c2a 100644 --- a/libs/horizon/src/dom/DOMOperator.ts +++ b/libs/horizon/src/dom/DOMOperator.ts @@ -225,7 +225,3 @@ export function unHideDom(tag: string, dom: Element | Text, props: Props) { dom.textContent = props; } } - -export function prePortal(portal: Element): void { - listenDelegatedEvents(portal); -} diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts index 7864426d..0d9dc259 100644 --- a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts +++ b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts @@ -3,7 +3,7 @@ import { updateCommonProp } from './UpdateCommonProp'; import { setStyles } from './StyleHandler'; import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding'; import { isEventProp } from '../validators/ValidateProps'; -import { getCurrentRoot } from '../../renderer/TreeBuilder'; +import { getCurrentRoot } from '../../renderer/RootStack'; // 初始化DOM属性和更新 DOM 属性 export function setDomProps(dom: Element, props: Object, isNativeTag: boolean, isInit: boolean): void { diff --git a/libs/horizon/src/event/EventBinding.ts b/libs/horizon/src/event/EventBinding.ts index 40e7625a..0623c9fb 100644 --- a/libs/horizon/src/event/EventBinding.ts +++ b/libs/horizon/src/event/EventBinding.ts @@ -48,22 +48,6 @@ function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, i return listener; } -// 监听所有委托事件 -export function listenDelegatedEvents(dom: Element) { - if (dom[listeningMarker]) { - // 不需要重复注册事件 - return; - } - dom[listeningMarker] = true; - - allDelegatedNativeEvents.forEach((nativeEvtName: string) => { - // 委托冒泡事件 - listenToNativeEvent(nativeEvtName, dom, false); - // 委托捕获事件 - listenToNativeEvent(nativeEvtName, dom, true); - }); -} - // 事件懒委托,当用户定义事件后,再进行委托到根节点 export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) { currentRoot.delegatedEvents.add(eventName); @@ -73,9 +57,17 @@ export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) { nativeEvents.forEach(nativeEvent => { const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent; - if (!currentRoot.delegatedNativeEvents[nativeFullName]) { + + // 事件存储在DOM节点属性,避免多个VNode(root和portal)对应同一个DOM, 造成事件重复监听 + let events = currentRoot.realNode.$EV; + + if (!events) { + events = (currentRoot.realNode as any).$EV = {}; + } + + if (!events[nativeFullName]) { const listener = listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture); - currentRoot.delegatedNativeEvents[nativeFullName] = listener; + events[nativeFullName] = listener; } }); } diff --git a/libs/horizon/src/renderer/RootStack.ts b/libs/horizon/src/renderer/RootStack.ts new file mode 100644 index 00000000..8bce59e6 --- /dev/null +++ b/libs/horizon/src/renderer/RootStack.ts @@ -0,0 +1,17 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved. + */ + +import { VNode } from './vnode/VNode'; +const currentRootStack: VNode[] = []; +export function getCurrentRoot() { + return currentRootStack[currentRootStack.length - 1]; +} + +export function pushCurrentRoot(root: VNode) { + return currentRootStack.push(root); +} + +export function popCurrentRoot() { + return currentRootStack.pop(); +} diff --git a/libs/horizon/src/renderer/TreeBuilder.ts b/libs/horizon/src/renderer/TreeBuilder.ts index 683c9bc3..7bc0eece 100644 --- a/libs/horizon/src/renderer/TreeBuilder.ts +++ b/libs/horizon/src/renderer/TreeBuilder.ts @@ -2,7 +2,7 @@ import type { VNode } from './Types'; import { callRenderQueueImmediate, pushRenderCallback } from './taskExecutor/RenderQueue'; import { updateVNode } from './vnode/VNodeCreator'; -import { TreeRoot, DomComponent, DomPortal } from './vnode/VNodeTags'; +import { DomComponent, DomPortal, TreeRoot } from './vnode/VNodeTags'; import { FlagUtils, InitFlag, Interrupted } from './vnode/VNodeFlags'; import { captureVNode } from './render/BaseComponent'; import { checkLoopingUpdateLimit, submitToRender } from './submit/Submit'; @@ -12,41 +12,39 @@ import componentRenders from './render'; import { BuildCompleted, BuildFatalErrored, - BuildInComplete, getBuildResult, + BuildInComplete, + getBuildResult, getStartVNode, setBuildResult, setProcessingClassVNode, - setStartVNode + setStartVNode, } from './GlobalVar'; import { ByAsync, BySync, - InRender, - InEvent, changeMode, checkMode, copyExecuteMode, + InEvent, + InRender, isExecuting, - setExecuteMode + setExecuteMode, } from './ExecuteMode'; -import { recoverParentContext, resetParentContext, resetNamespaceCtx, setNamespaceCtx } from './ContextSaver'; +import { recoverParentContext, resetNamespaceCtx, resetParentContext, setNamespaceCtx } from './ContextSaver'; import { updateChildShouldUpdate, updateParentsChildShouldUpdate, - updateShouldUpdateOfTree + updateShouldUpdateOfTree, } from './vnode/VNodeShouldUpdate'; import { getPathArr } from './utils/vNodePath'; import { injectUpdater } from '../external/devtools'; +import { popCurrentRoot, pushCurrentRoot } from './RootStack'; // 不可恢复错误 let unrecoverableErrorDuringBuild: any = null; // 当前运行的vNode节点 let processing: VNode | null = null; -let currentRoot: VNode | null = null; -export function getCurrentRoot() { - return currentRoot; -} export function setProcessing(vNode: VNode | null) { processing = vNode; @@ -280,7 +278,7 @@ function buildVNodeTree(treeRoot: VNode) { // 总体任务入口 function renderFromRoot(treeRoot) { runAsyncEffects(); - currentRoot = treeRoot; + pushCurrentRoot(treeRoot); // 1. 构建vNode树 buildVNodeTree(treeRoot); @@ -291,8 +289,7 @@ function renderFromRoot(treeRoot) { // 2. 提交变更 submitToRender(treeRoot); - currentRoot = null; - + popCurrentRoot(); if (window.__HORIZON_DEV_HOOK__) { const hook = window.__HORIZON_DEV_HOOK__; // injector.js 可能在 Horizon 代码之后加载,此时无 __HORIZON_DEV_HOOK__ 全局变量 diff --git a/libs/horizon/src/renderer/render/DomPortal.ts b/libs/horizon/src/renderer/render/DomPortal.ts index baf4b500..719a5b60 100644 --- a/libs/horizon/src/renderer/render/DomPortal.ts +++ b/libs/horizon/src/renderer/render/DomPortal.ts @@ -1,18 +1,16 @@ import type { VNode } from '../Types'; import { resetNamespaceCtx, setNamespaceCtx } from '../ContextSaver'; import { createChildrenByDiff } from '../diff/nodeDiffComparator'; -import { prePortal } from '../../dom/DOMOperator'; +import { popCurrentRoot, pushCurrentRoot } from '../RootStack'; export function bubbleRender(processing: VNode) { resetNamespaceCtx(processing); - - if (processing.isCreated) { - prePortal(processing.realNode); - } + popCurrentRoot(); } function capturePortalComponent(processing: VNode) { setNamespaceCtx(processing, processing.realNode); + pushCurrentRoot(processing); const newElements = processing.props; if (processing.isCreated) { diff --git a/libs/horizon/src/renderer/vnode/VNode.ts b/libs/horizon/src/renderer/vnode/VNode.ts index 03937e4a..18b4d528 100644 --- a/libs/horizon/src/renderer/vnode/VNode.ts +++ b/libs/horizon/src/renderer/vnode/VNode.ts @@ -78,7 +78,6 @@ export class VNode { // 根节点数据 toUpdateNodes: Set | null; // 保存要更新的节点 delegatedEvents: Set; - delegatedNativeEvents: Record void>; belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode,处理ref的时候使用 @@ -100,7 +99,6 @@ export class VNode { this.task = null; this.toUpdateNodes = new Set(); this.delegatedEvents = new Set(); - this.delegatedNativeEvents = {}; this.updates = null; this.stateCallbacks = null; this.state = null; @@ -137,6 +135,7 @@ export class VNode { case DomPortal: this.realNode = null; this.context = null; + this.delegatedEvents = new Set(); this.src = null; break; case DomComponent: diff --git a/scripts/__tests__/ComponentTest/PortalComponent.test.js b/scripts/__tests__/ComponentTest/PortalComponent.test.js index 77811c2d..05908f18 100755 --- a/scripts/__tests__/ComponentTest/PortalComponent.test.js +++ b/scripts/__tests__/ComponentTest/PortalComponent.test.js @@ -3,7 +3,7 @@ import { getLogUtils } from '../jest/testUtils'; describe('PortalComponent Test', () => { const LogUtils = getLogUtils(); - + it('将子节点渲染到存在于父组件以外的 DOM 节点', () => { const portalRoot = document.createElement('div'); @@ -202,4 +202,37 @@ describe('PortalComponent Test', () => { 'bubble click event' ]); }); -}); \ No newline at end of file + + it('Create portal at app root should not add event listener multiple times', () => { + const btnRef = Horizon.createRef(); + class PortalApp extends Horizon.Component { + constructor(props) { + super(props); + } + + render() { + return Horizon.createPortal( + this.props.child, + container, + ); + } + } + const onClick = jest.fn(); + + class App extends Horizon.Component { + constructor(props) { + super(props); + } + + render() { + return
+ + +
; + } + } + Horizon.render(, container); + btnRef.current.click(); + expect(onClick).toHaveBeenCalledTimes(1); + }); +});