Match-id-53306a6f1b254950ca8a57da1d3ba3e232d8492f

This commit is contained in:
* 2022-09-13 17:45:35 +08:00 committed by *
commit b569d2114d
6 changed files with 142 additions and 87 deletions

View File

@ -86,7 +86,7 @@ function removeRootEventLister(container: Container) {
// 卸载入口
function destroy(container: Container): boolean {
if (container._treeRoot) {
if (container && container._treeRoot) {
syncUpdates(() => {
executeRender(null, container, () => {
removeRootEventLister(container);

View File

@ -7,7 +7,6 @@ import type { VNode, ContextType } from './Types';
import type { Container } from '../dom/DOMOperator';
import { getNSCtx } from '../dom/DOMOperator';
import { ContextProvider } from './vnode/VNodeTags';
// 保存的是“http://www.w3.org/1999/xhtml”或“http://www.w3.org/2000/svg”
// 用于识别是使用document.createElement()还是使用document.createElementNS()创建DOM
@ -44,32 +43,3 @@ export function resetContext(providerVNode: VNode) {
context.value = providerVNode.context;
}
// 在局部更新时从上到下恢复父节点的context
export function recoverParentContext(vNode: VNode) {
const contextProviders: VNode[] = [];
let parent = vNode.parent;
while (parent !== null) {
if (parent.tag === ContextProvider) {
contextProviders.unshift(parent);
}
parent = parent.parent;
}
contextProviders.forEach(node => {
setContext(node, node.props.value);
});
}
// 在局部更新时从下到上重置父节点的context
export function resetParentContext(vNode: VNode) {
let parent = vNode.parent;
while (parent !== null) {
if (parent.tag === ContextProvider) {
resetContext(parent);
}
parent = parent.parent;
}
}

View File

@ -2,7 +2,7 @@ import type { VNode } from './Types';
import { callRenderQueueImmediate, pushRenderCallback } from './taskExecutor/RenderQueue';
import { updateVNode } from './vnode/VNodeCreator';
import { DomComponent, DomPortal, TreeRoot } from './vnode/VNodeTags';
import { ContextProvider, DomComponent, DomPortal, TreeRoot } from './vnode/VNodeTags';
import { FlagUtils, InitFlag, Interrupted } from './vnode/VNodeFlags';
import { captureVNode } from './render/BaseComponent';
import { checkLoopingUpdateLimit, submitToRender } from './submit/Submit';
@ -30,7 +30,12 @@ import {
isExecuting,
setExecuteMode,
} from './ExecuteMode';
import { recoverParentContext, resetNamespaceCtx, resetParentContext, setNamespaceCtx } from './ContextSaver';
import {
resetContext,
resetNamespaceCtx,
setContext,
setNamespaceCtx,
} from './ContextSaver';
import {
updateChildShouldUpdate,
updateParentsChildShouldUpdate,
@ -244,7 +249,7 @@ function buildVNodeTree(treeRoot: VNode) {
}
// 恢复父节点的context
recoverParentContext(startVNode);
recoverTreeContext(startVNode);
}
// 重置环境变量,为重新进行深度遍历做准备
@ -272,7 +277,7 @@ function buildVNodeTree(treeRoot: VNode) {
}
if (startVNode.tag !== TreeRoot) { // 不是根节点
// 恢复父节点的context
resetParentContext(startVNode);
resetTreeContext(startVNode);
}
setProcessingClassVNode(null);
@ -280,6 +285,39 @@ function buildVNodeTree(treeRoot: VNode) {
setExecuteMode(preMode);
}
// 在局部更新时从上到下恢复父节点的context和PortalStack
function recoverTreeContext(vNode: VNode) {
const contextProviders: VNode[] = [];
let parent = vNode.parent;
while (parent !== null) {
if (parent.tag === ContextProvider) {
contextProviders.unshift(parent);
}
if(parent.tag === DomPortal){
pushCurrentRoot(parent);
}
parent = parent.parent;
}
contextProviders.forEach(node => {
setContext(node, node.props.value);
});
}
// 在局部更新时从下到上重置父节点的context
function resetTreeContext(vNode: VNode) {
let parent = vNode.parent;
while (parent !== null) {
if (parent.tag === ContextProvider) {
resetContext(parent);
}
if(parent.tag === DomPortal){
popCurrentRoot();
}
parent = parent.parent;
}
}
// 总体任务入口
function renderFromRoot(treeRoot) {
runAsyncEffects();

View File

@ -7,9 +7,10 @@ import { onlyUpdateChildVNodes } from '../vnode/VNodeCreator';
import componentRenders from './index';
import { setProcessingVNode } from '../GlobalVar';
import { clearVNodeObservers } from '../../horizonx/store/StoreHandler';
import { pushCurrentRoot } from '../RootStack';
// 复用vNode时也需对stack进行处理
function handlerContext(processing: VNode) {
// 复用vNode时也需对树的上下文值处理如contextportal, namespaceContext
function setTreeContextValue(processing: VNode) {
switch (processing.tag) {
case TreeRoot:
setNamespaceCtx(processing, processing.realNode);
@ -19,6 +20,7 @@ function handlerContext(processing: VNode) {
break;
case DomPortal:
setNamespaceCtx(processing, processing.realNode);
pushCurrentRoot(processing);
break;
case ContextProvider: {
const newValue = processing.props.value;
@ -36,7 +38,7 @@ export function captureVNode(processing: VNode): VNode | null {
// 该vNode没有变化不用进入capture直接复用。
if (!processing.isCreated && processing.oldProps === processing.props && !processing.shouldUpdate) {
// 复用还需对stack进行处理
handlerContext(processing);
setTreeContextValue(processing);
return onlyUpdateChildVNodes(processing);
}

View File

@ -1,5 +1,6 @@
import * as Horizon from '@cloudsop/horizon/index.ts';
import { getLogUtils } from '../jest/testUtils';
import dispatchChangeEvent from '../utils/dispatchChangeEvent';
describe('PortalComponent Test', () => {
const LogUtils = getLogUtils();
@ -14,12 +15,10 @@ describe('PortalComponent Test', () => {
}
render() {
return Horizon.createPortal(
this.props.child,
this.element,
);
return Horizon.createPortal(this.props.child, this.element);
}
}
Horizon.render(<PortalApp child={<div>PortalApp</div>} />, container);
expect(container.textContent).toBe('');
// <div>PortalApp</div>被渲染到了portalRoot而非container
@ -43,17 +42,12 @@ describe('PortalComponent Test', () => {
render() {
return [
Horizon.createPortal(
this.props.child,
this.element,
),
Horizon.createPortal(
this.props.child,
this.newElement,
)
Horizon.createPortal(this.props.child, this.element),
Horizon.createPortal(this.props.child, this.newElement),
];
}
}
Horizon.render(<PortalApp child={<div>PortalApp</div>} />, container);
expect(container.textContent).toBe('');
// <div>PortalApp</div>被渲染到了portalRoot而非container
@ -82,21 +76,16 @@ describe('PortalComponent Test', () => {
render() {
return [
<div>PortalApp1st</div>,
Horizon.createPortal([
<div>PortalApp4</div>,
Horizon.createPortal(
this.props.child,
this.element3rd,
),
], this.element),
<div>PortalApp2nd</div>,
Horizon.createPortal(
this.props.child,
this.newElement,
)
[<div>PortalApp4</div>, Horizon.createPortal(this.props.child, this.element3rd)],
this.element
),
<div>PortalApp2nd</div>,
Horizon.createPortal(this.props.child, this.newElement),
];
}
}
Horizon.render(<PortalApp child={<div>PortalApp</div>} />, container);
expect(container.textContent).toBe('PortalApp1stPortalApp2nd');
// <div>PortalApp4</div>会挂载在this.element上
@ -120,25 +109,23 @@ describe('PortalComponent Test', () => {
}
render() {
return Horizon.createPortal(
this.props.child,
this.element,
);
return Horizon.createPortal(this.props.child, this.element);
}
}
Horizon.render(<PortalApp key='portal' child={<div>PortalApp</div>} />, container);
Horizon.render(<PortalApp key="portal" child={<div>PortalApp</div>} />, container);
expect(container.textContent).toBe('');
expect(portalRoot.textContent).toBe('PortalApp');
Horizon.render(<PortalApp key='portal' child={<div>AppPortal</div>} />, container);
Horizon.render(<PortalApp key="portal" child={<div>AppPortal</div>} />, container);
expect(container.textContent).toBe('');
expect(portalRoot.textContent).toBe('AppPortal');
Horizon.render(<PortalApp key='portal' child={['por', 'tal']} />, container);
Horizon.render(<PortalApp key="portal" child={['por', 'tal']} />, container);
expect(container.textContent).toBe('');
expect(portalRoot.textContent).toBe('portal');
Horizon.render(<PortalApp key='portal' child={null} />, container);
Horizon.render(<PortalApp key="portal" child={null} />, container);
expect(container.textContent).toBe('');
expect(portalRoot.textContent).toBe('');
@ -158,10 +145,7 @@ describe('PortalComponent Test', () => {
}
render() {
return Horizon.createPortal(
this.props.child,
this.element,
);
return Horizon.createPortal(this.props.child, this.element);
}
}
@ -173,7 +157,6 @@ describe('PortalComponent Test', () => {
);
};
const App = () => {
const handleClick = () => {
LogUtils.log('bubble click event');
@ -185,9 +168,7 @@ describe('PortalComponent Test', () => {
return (
<div onClickCapture={handleCaptureClick()} onClick={handleClick()}>
<PortalApp child={<Child />}>
</PortalApp>
<PortalApp child={<Child />}></PortalApp>
</div>
);
};
@ -199,24 +180,23 @@ describe('PortalComponent Test', () => {
expect(LogUtils.getAndClear()).toEqual([
// 从外到内先捕获再冒泡
'capture click event',
'bubble click event'
'bubble click event',
]);
});
it('Create portal at app root should not add event listener multiple times', () => {
const btnRef = Horizon.createRef();
const btnRef = Horizon.createRef();
class PortalApp extends Horizon.Component {
constructor(props) {
super(props);
}
render() {
return Horizon.createPortal(
this.props.child,
container,
);
return Horizon.createPortal(this.props.child, container);
}
}
const onClick = jest.fn();
class App extends Horizon.Component {
@ -225,14 +205,70 @@ describe('PortalComponent Test', () => {
}
render() {
return <div>
<button onClick={onClick} ref={btnRef}></button>
<PortalApp />
</div>;
return (
<div>
<button onClick={onClick} ref={btnRef}></button>
<PortalApp />
</div>
);
}
}
Horizon.render(<App />, container);
btnRef.current.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
it('#76 Portal onChange should activate', () => {
class Dialog extends Horizon.Component {
node;
constructor(props) {
super(props);
this.node = window.document.createElement('div');
window.document.body.appendChild(this.node);
}
render() {
return Horizon.createPortal(this.props.children, this.node);
}
}
let showPortalInput;
const fn = jest.fn();
const inputRef = Horizon.createRef();
function App() {
const Input = () => {
const [show, setShow] = Horizon.useState(false);
showPortalInput = setShow;
Horizon.useEffect(() => {
setTimeout(() => {
setShow(true);
}, 0);
}, []);
if (!show) {
return null;
}
return <input onChange={fn} ref={inputRef} />;
};
return (
<div>
<Dialog>
<Input />
</Dialog>
</div>
);
}
Horizon.render(<App />, container);
showPortalInput(true);
jest.advanceTimersToNextTimer();
dispatchChangeEvent(inputRef.current, 'test');
expect(fn).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,9 @@
/*
* Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
*/
export default function dispatchChangeEvent(inputEle, value) {
const nativeInputSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeInputSetter.call(inputEle, value);
inputEle.dispatchEvent(new Event('input', { bubbles: true}));
}