Match-id-529f7b4df4c14005c057b34b7fba761b8bbd74c3

This commit is contained in:
* 2022-09-05 17:23:34 +08:00 committed by *
commit b69ebe308a
9 changed files with 84 additions and 51 deletions

View File

@ -71,13 +71,14 @@ function findDOMNode(domOrEle?: Element): null | Element | Text {
// 情况根节点监听器 // 情况根节点监听器
function removeRootEventLister(container: Container) { function removeRootEventLister(container: Container) {
const root = container._treeRoot; const events = (container._treeRoot as any).$EV;
if (root) { if (events) {
Object.keys(root.delegatedNativeEvents).forEach(event => { Object.keys(events).forEach(event => {
const listener = root.delegatedNativeEvents[event]; const listener = events[event];
if (listener) { if (listener) {
container.removeEventListener(event, listener); container.removeEventListener(event, listener);
events[event] = null;
} }
}); });
} }

View File

@ -225,7 +225,3 @@ export function unHideDom(tag: string, dom: Element | Text, props: Props) {
dom.textContent = props; dom.textContent = props;
} }
} }
export function prePortal(portal: Element): void {
listenDelegatedEvents(portal);
}

View File

@ -3,7 +3,7 @@ import { updateCommonProp } from './UpdateCommonProp';
import { setStyles } from './StyleHandler'; import { setStyles } from './StyleHandler';
import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding'; import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding';
import { isEventProp } from '../validators/ValidateProps'; import { isEventProp } from '../validators/ValidateProps';
import { getCurrentRoot } from '../../renderer/TreeBuilder'; import { getCurrentRoot } from '../../renderer/RootStack';
// 初始化DOM属性和更新 DOM 属性 // 初始化DOM属性和更新 DOM 属性
export function setDomProps(dom: Element, props: Object, isNativeTag: boolean, isInit: boolean): void { export function setDomProps(dom: Element, props: Object, isNativeTag: boolean, isInit: boolean): void {

View File

@ -48,22 +48,6 @@ function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, i
return listener; 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) { export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) {
currentRoot.delegatedEvents.add(eventName); currentRoot.delegatedEvents.add(eventName);
@ -73,9 +57,17 @@ export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) {
nativeEvents.forEach(nativeEvent => { nativeEvents.forEach(nativeEvent => {
const nativeFullName = isCapture ? nativeEvent + 'capture' : 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); const listener = listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture);
currentRoot.delegatedNativeEvents[nativeFullName] = listener; events[nativeFullName] = listener;
} }
}); });
} }

View File

@ -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();
}

View File

@ -2,7 +2,7 @@ import type { VNode } from './Types';
import { callRenderQueueImmediate, pushRenderCallback } from './taskExecutor/RenderQueue'; import { callRenderQueueImmediate, pushRenderCallback } from './taskExecutor/RenderQueue';
import { updateVNode } from './vnode/VNodeCreator'; 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 { FlagUtils, InitFlag, Interrupted } from './vnode/VNodeFlags';
import { captureVNode } from './render/BaseComponent'; import { captureVNode } from './render/BaseComponent';
import { checkLoopingUpdateLimit, submitToRender } from './submit/Submit'; import { checkLoopingUpdateLimit, submitToRender } from './submit/Submit';
@ -12,41 +12,39 @@ import componentRenders from './render';
import { import {
BuildCompleted, BuildCompleted,
BuildFatalErrored, BuildFatalErrored,
BuildInComplete, getBuildResult, BuildInComplete,
getBuildResult,
getStartVNode, getStartVNode,
setBuildResult, setBuildResult,
setProcessingClassVNode, setProcessingClassVNode,
setStartVNode setStartVNode,
} from './GlobalVar'; } from './GlobalVar';
import { import {
ByAsync, ByAsync,
BySync, BySync,
InRender,
InEvent,
changeMode, changeMode,
checkMode, checkMode,
copyExecuteMode, copyExecuteMode,
InEvent,
InRender,
isExecuting, isExecuting,
setExecuteMode setExecuteMode,
} from './ExecuteMode'; } from './ExecuteMode';
import { recoverParentContext, resetParentContext, resetNamespaceCtx, setNamespaceCtx } from './ContextSaver'; import { recoverParentContext, resetNamespaceCtx, resetParentContext, setNamespaceCtx } from './ContextSaver';
import { import {
updateChildShouldUpdate, updateChildShouldUpdate,
updateParentsChildShouldUpdate, updateParentsChildShouldUpdate,
updateShouldUpdateOfTree updateShouldUpdateOfTree,
} from './vnode/VNodeShouldUpdate'; } from './vnode/VNodeShouldUpdate';
import { getPathArr } from './utils/vNodePath'; import { getPathArr } from './utils/vNodePath';
import { injectUpdater } from '../external/devtools'; import { injectUpdater } from '../external/devtools';
import { popCurrentRoot, pushCurrentRoot } from './RootStack';
// 不可恢复错误 // 不可恢复错误
let unrecoverableErrorDuringBuild: any = null; let unrecoverableErrorDuringBuild: any = null;
// 当前运行的vNode节点 // 当前运行的vNode节点
let processing: VNode | null = null; let processing: VNode | null = null;
let currentRoot: VNode | null = null;
export function getCurrentRoot() {
return currentRoot;
}
export function setProcessing(vNode: VNode | null) { export function setProcessing(vNode: VNode | null) {
processing = vNode; processing = vNode;
@ -280,7 +278,7 @@ function buildVNodeTree(treeRoot: VNode) {
// 总体任务入口 // 总体任务入口
function renderFromRoot(treeRoot) { function renderFromRoot(treeRoot) {
runAsyncEffects(); runAsyncEffects();
currentRoot = treeRoot; pushCurrentRoot(treeRoot);
// 1. 构建vNode树 // 1. 构建vNode树
buildVNodeTree(treeRoot); buildVNodeTree(treeRoot);
@ -291,8 +289,7 @@ function renderFromRoot(treeRoot) {
// 2. 提交变更 // 2. 提交变更
submitToRender(treeRoot); submitToRender(treeRoot);
currentRoot = null; popCurrentRoot();
if (window.__HORIZON_DEV_HOOK__) { if (window.__HORIZON_DEV_HOOK__) {
const hook = window.__HORIZON_DEV_HOOK__; const hook = window.__HORIZON_DEV_HOOK__;
// injector.js 可能在 Horizon 代码之后加载,此时无 __HORIZON_DEV_HOOK__ 全局变量 // injector.js 可能在 Horizon 代码之后加载,此时无 __HORIZON_DEV_HOOK__ 全局变量

View File

@ -1,18 +1,16 @@
import type { VNode } from '../Types'; import type { VNode } from '../Types';
import { resetNamespaceCtx, setNamespaceCtx } from '../ContextSaver'; import { resetNamespaceCtx, setNamespaceCtx } from '../ContextSaver';
import { createChildrenByDiff } from '../diff/nodeDiffComparator'; import { createChildrenByDiff } from '../diff/nodeDiffComparator';
import { prePortal } from '../../dom/DOMOperator'; import { popCurrentRoot, pushCurrentRoot } from '../RootStack';
export function bubbleRender(processing: VNode) { export function bubbleRender(processing: VNode) {
resetNamespaceCtx(processing); resetNamespaceCtx(processing);
popCurrentRoot();
if (processing.isCreated) {
prePortal(processing.realNode);
}
} }
function capturePortalComponent(processing: VNode) { function capturePortalComponent(processing: VNode) {
setNamespaceCtx(processing, processing.realNode); setNamespaceCtx(processing, processing.realNode);
pushCurrentRoot(processing);
const newElements = processing.props; const newElements = processing.props;
if (processing.isCreated) { if (processing.isCreated) {

View File

@ -78,7 +78,6 @@ export class VNode {
// 根节点数据 // 根节点数据
toUpdateNodes: Set<VNode> | null; // 保存要更新的节点 toUpdateNodes: Set<VNode> | null; // 保存要更新的节点
delegatedEvents: Set<string>; delegatedEvents: Set<string>;
delegatedNativeEvents: Record<string, () => void>;
belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode处理ref的时候使用 belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode处理ref的时候使用
@ -100,7 +99,6 @@ export class VNode {
this.task = null; this.task = null;
this.toUpdateNodes = new Set<VNode>(); this.toUpdateNodes = new Set<VNode>();
this.delegatedEvents = new Set<string>(); this.delegatedEvents = new Set<string>();
this.delegatedNativeEvents = {};
this.updates = null; this.updates = null;
this.stateCallbacks = null; this.stateCallbacks = null;
this.state = null; this.state = null;
@ -137,6 +135,7 @@ export class VNode {
case DomPortal: case DomPortal:
this.realNode = null; this.realNode = null;
this.context = null; this.context = null;
this.delegatedEvents = new Set<string>();
this.src = null; this.src = null;
break; break;
case DomComponent: case DomComponent:

View File

@ -3,7 +3,7 @@ import { getLogUtils } from '../jest/testUtils';
describe('PortalComponent Test', () => { describe('PortalComponent Test', () => {
const LogUtils = getLogUtils(); const LogUtils = getLogUtils();
it('将子节点渲染到存在于父组件以外的 DOM 节点', () => { it('将子节点渲染到存在于父组件以外的 DOM 节点', () => {
const portalRoot = document.createElement('div'); const portalRoot = document.createElement('div');
@ -202,4 +202,37 @@ describe('PortalComponent Test', () => {
'bubble click event' 'bubble click event'
]); ]);
}); });
});
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 <div>
<button onClick={onClick} ref={btnRef}></button>
<PortalApp />
</div>;
}
}
Horizon.render(<App />, container);
btnRef.current.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
});