Match-id-6457906eabfeed181e7563bd49efe9db3c404a5c

This commit is contained in:
* 2023-05-29 15:09:12 +08:00
commit 77747e72ee
14 changed files with 561 additions and 11 deletions

View File

@ -1,3 +1,6 @@
## 0.0.51 (2023-05-29)
- **core**: 增加mouseenter和mouseleave事件代理机制
## 0.0.50 (2023-05-23)
- **core**: 解决IE11不兼容Symbol问题

View File

@ -4,7 +4,7 @@
"keywords": [
"horizon"
],
"version": "0.0.50",
"version": "0.0.51",
"homepage": "",
"bugs": "",
"main": "index.js",

View File

@ -19,6 +19,7 @@ import type { Container } from './DOMOperator';
import { isElement } from './utils/Common';
import { findDOMByClassInst } from '../renderer/vnode/VNodeUtils';
import { Callback } from '../renderer/UpdateHandler';
import { listenSimulatedDelegatedEvents } from '../event/EventBinding';
function createRoot(children: any, container: Container, callback?: Callback) {
// 清空容器
@ -31,6 +32,7 @@ function createRoot(children: any, container: Container, callback?: Callback) {
// 调度器创建根节点并给容器dom赋vNode结构体
const treeRoot = createTreeRootVNode(container);
container._treeRoot = treeRoot;
listenSimulatedDelegatedEvents(treeRoot);
// 执行回调
if (typeof callback === 'function') {

View File

@ -16,7 +16,7 @@
/**
*
*/
import { allDelegatedHorizonEvents, allDelegatedNativeEvents } from './EventHub';
import { allDelegatedHorizonEvents, simulatedDelegatedEvents } from './EventHub';
import { isDocument } from '../dom/utils/Common';
import { getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys';
import { asyncUpdates, runDiscreteUpdates } from '../renderer/TreeBuilder';
@ -24,8 +24,6 @@ import { handleEventMain } from './HorizonEventMain';
import { decorateNativeEvent } from './EventWrapper';
import { VNode } from '../renderer/vnode/VNode';
const listeningMarker = '_horizonListening' + Math.random().toString(36).slice(4);
// 触发委托事件
function triggerDelegatedEvent(
nativeEvtName: string,
@ -64,6 +62,13 @@ function isCaptureEvent(horizonEventName) {
return horizonEventName.slice(-7) === 'Capture';
}
// 利用冒泡事件模拟不冒泡事件,需要直接在根节点绑定
export function listenSimulatedDelegatedEvents(root: VNode) {
for (let i = 0; i < simulatedDelegatedEvents.length; i++) {
lazyDelegateOnRoot(root, simulatedDelegatedEvents[i]);
}
}
// 事件懒委托,当用户定义事件后,再进行委托到根节点
export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) {
currentRoot.delegatedEvents.add(eventName);

View File

@ -15,6 +15,9 @@
// 需要委托的horizon事件和原生事件对应关系
export const allDelegatedHorizonEvents = new Map();
// 模拟委托事件,不冒泡事件需要利用其他事件来触发冒泡过程
export const simulatedDelegatedEvents = ['onMouseEnter', 'onMouseLeave'];
// 所有委托的原生事件集合
export const allDelegatedNativeEvents = new Set();
@ -49,6 +52,8 @@ export const horizonEventToNativeMap = new Map([
['onCompositionUpdate', ['compositionupdate']],
['onChange', ['change', 'click', 'focusout', 'input']],
['onSelect', ['select']],
['onMouseEnter', ['mouseout', 'mouseover']],
['onMouseLeave', ['mouseout', 'mouseover']],
['onAnimationEnd', ['animationend']],
['onAnimationIteration', ['animationiteration']],

View File

@ -37,6 +37,9 @@ export class WrappedEvent {
key: string;
currentTarget: EventTarget | null = null;
target: HTMLElement;
relatedTarget: HTMLElement;
stopPropagation: () => void;
preventDefault: () => void;

View File

@ -31,6 +31,7 @@ import { getDomTag } from '../dom/utils/Common';
import { updateInputHandlerIfChanged } from '../dom/valueHandler/ValueChangeHandler';
import { getDom } from '../dom/DOMInternalKeys';
import { recordChangeEventTargets, shouldControlValue, tryControlValue } from './FormValueController';
import { getMouseEnterListeners } from './MouseEvent';
// web规范鼠标右键key值
const RIGHT_MOUSE_BUTTON = 2;
@ -141,18 +142,26 @@ function triggerHorizonEvents(
const target = nativeEvent.target || nativeEvent.srcElement!;
// 触发普通委托事件
let listenerList: ListenerUnitList = getCommonListeners(nativeEvtName, vNode, nativeEvent, target, isCapture);
const listenerList: ListenerUnitList = getCommonListeners(nativeEvtName, vNode, nativeEvent, target, isCapture);
let mouseEnterListeners: ListenerUnitList = [];
if (horizonEventToNativeMap.get('onMouseEnter')!.includes(nativeEvtName)) {
mouseEnterListeners = getMouseEnterListeners(
nativeEvtName,
vNode,
nativeEvent,
target,
);
}
let changeEvents: ListenerUnitList = [];
// 触发特殊handler委托事件
if (!isCapture && horizonEventToNativeMap.get('onChange')!.includes(nativeEvtName)) {
const changeListeners = getChangeListeners(nativeEvtName, nativeEvent, vNode, target);
if (changeListeners.length) {
listenerList = listenerList.concat(changeListeners);
}
changeEvents = getChangeListeners(nativeEvtName, nativeEvent, vNode, target);
}
// 处理触发的事件队列
processListeners(listenerList);
processListeners([...listenerList, ...mouseEnterListeners, ...changeEvents]);
}
// 其他事件正在执行中标记

View File

@ -86,3 +86,90 @@ export function getListenersFromTree(
return listeners;
}
// 获取enter和leave事件队列
export function collectMouseListeners(
leaveEvent: null | WrappedEvent,
enterEvent: null | WrappedEvent,
from: VNode | null,
to: VNode | null,
): ListenerUnitList {
// 确定公共父节点,作为在树上遍历的终点
const commonParent = from && to ? getCommonAncestor(from, to) : null;
let leaveEventList: ListenerUnitList = [];
if (from && leaveEvent) {
// 遍历树获取绑定的leave事件
leaveEventList = getMouseListenersFromTree(
leaveEvent,
from,
commonParent,
);
}
let enterEventList: ListenerUnitList = [];
if (to && enterEvent) {
// 先触发父节点enter事件所以需要逆序
enterEventList = getMouseListenersFromTree(
enterEvent,
to,
commonParent,
).reverse();
}
return [...leaveEventList, ...enterEventList];
}
function getMouseListenersFromTree(
event: WrappedEvent,
target: VNode,
commonParent: VNode | null,
): ListenerUnitList {
const registrationName = event.customEventName;
const listeners: ListenerUnitList = [];
let vNode = target;
while (vNode !== null) {
// commonParent作为终点
if (vNode === commonParent) {
break;
}
const {realNode, tag} = vNode;
if (tag === DomComponent && realNode !== null) {
const currentTarget = realNode;
const listener = getListenerFromVNode(vNode, registrationName);
if (listener) {
listeners.push({
vNode,
listener,
currentTarget,
event,
});
}
}
vNode = vNode.parent;
}
return listeners;
}
// 寻找两个节点的共同最近祖先如果没有则返回null
function getCommonAncestor(instA: VNode, instB: VNode): VNode | null {
const parentsSet = new Set<VNode>();
for (let tempA: VNode | null = instA; tempA; tempA = getParent(tempA)) {
parentsSet.add(tempA);
}
for (let tempB: VNode | null = instB; tempB; tempB = getParent(tempB)) {
if (parentsSet.has(tempB)) {
return tempB;
}
}
return null;
}
// 获取父节点
function getParent(inst: VNode | null): VNode | null {
if (inst === null) {
return null;
}
do {
inst = inst.parent;
} while (inst && inst.tag !== DomComponent);
return inst || null;
}

View File

@ -0,0 +1,109 @@
/*
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
*
* openGauss is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { getNearestVNode } from '../dom/DOMInternalKeys';
import { WrappedEvent } from './EventWrapper';
import { VNode } from '../renderer/vnode/VNode';
import { AnyNativeEvent, ListenerUnitList } from './Types';
import { DomComponent, DomText } from '../renderer/vnode/VNodeTags';
import { collectMouseListeners } from './ListenerGetter';
import { getNearestMountedVNode } from './utils';
/**
* mouseEnter和mouseLeave事件不冒泡
* mouseoutmouseover事件的mouseEnter和mouseLeave事件
*
* 1. mouseout和mouseover事件
* 2.
* 3. enter和leave事件
* 4.
* 5. treeNodemouseEnter和mouseLeave监听方法
* mouseOut事件由D->C, A节点作为公共父节点 DB的mouseLeave事件和C节点的mouseEnter事件
* A
* / \
* B C
* / \
* D E
*
*/
function getWrapperEvents(nativeEventTarget, fromInst, toInst, nativeEvent, targetInst): (WrappedEvent | null)[] {
const vWindow = nativeEventTarget.window === nativeEventTarget ? nativeEventTarget : nativeEventTarget.ownerDocument.defaultView;
// 起点或者终点为空的话默认值为所在window
const fromNode = fromInst?.realNode || vWindow;
const toNode = toInst?.realNode || vWindow;
let leave: WrappedEvent | null = null;
let enter: WrappedEvent | null = null;
const nativeTargetInst = getNearestVNode(nativeEventTarget);
// 在Mounted的dom节点上render一个子组件系统中存在两个根节点子节点的mouseout事件触发两次取离target近的根节点生效
if (nativeTargetInst === targetInst) {
leave = new WrappedEvent('onMouseLeave', 'mouseleave', nativeEvent);
leave.target = fromNode;
leave.relatedTarget = toNode;
enter = new WrappedEvent('onMouseEnter', 'mouseenter', nativeEvent);
enter.target = toNode;
enter.relatedTarget = fromNode;
}
return [leave, enter];
}
function getEndpointVNode(
domEventName: string,
targetInst: null | VNode,
nativeEvent: AnyNativeEvent,
): (VNode | null)[] {
let fromVNode;
let toVNode;
if (domEventName === 'mouseover') {
fromVNode = null;
toVNode = targetInst;
} else {
const related = nativeEvent.relatedTarget || nativeEvent.toElement;
fromVNode = targetInst;
toVNode = related ? getNearestVNode(related) : null;
if (toVNode !== null) {
const nearestMounted = getNearestMountedVNode(toVNode);
if (toVNode !== nearestMounted || (toVNode.tag !== DomComponent && toVNode.tag !== DomText)) {
toVNode = null;
}
}
}
return [fromVNode, toVNode];
}
export function getMouseEnterListeners(
domEventName: string,
targetInst: null | VNode,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
): ListenerUnitList {
// 获取起点和终点的VNode
const [fromVNode, toVNode] = getEndpointVNode(domEventName, targetInst, nativeEvent);
if (fromVNode === toVNode) {
return [];
}
// 获取包装后的leave和enter事件
const [leave, enter] = getWrapperEvents(nativeEventTarget, fromVNode, toVNode, nativeEvent, targetInst);
// 收集事件的监听方法
return collectMouseListeners(leave, enter, fromVNode, toVNode);
}

View File

@ -13,6 +13,10 @@
* See the Mulan PSL v2 for more details.
*/
import { VNode } from '../renderer/vnode/VNode';
import { Addition, FlagUtils } from '../renderer/vnode/VNodeFlags';
import { TreeRoot } from '../renderer/vnode/VNodeTags';
export function isInputElement(dom?: HTMLElement): boolean {
return dom instanceof HTMLInputElement || dom instanceof HTMLTextAreaElement;
}
@ -20,6 +24,26 @@ export function isInputElement(dom?: HTMLElement): boolean {
export function setPropertyWritable(obj, propName) {
const desc = Object.getOwnPropertyDescriptor(obj, propName);
if (!desc || !desc.writable) {
Object.defineProperty(obj, propName, { writable: true });
Object.defineProperty(obj, propName, {writable: true});
}
}
// 获取离 vNode 最近的已挂载 vNode包含它自己
export function getNearestMountedVNode(vNode: VNode): null | VNode {
let node = vNode;
let target = vNode;
// 如果没有alternate说明是可能是未插入的新树需要处理插入的副作用。
while (node.parent) {
// 存在更新,节点未挂载,查找父节点,但是父节点也可能未挂载,需要继续往上查找无更新节点
if (FlagUtils.hasFlag(node, Addition)) {
target = node.parent;
}
node = node.parent;
}
// 如果根节点是 Dom 类型节点,表示已经挂载
if (node.tag === TreeRoot) {
return target;
}
// 如果没有找到根节点意味着Tree已经卸载或者未挂载
return null;
}

View File

@ -17,9 +17,11 @@ import type { VNode } from '../Types';
import { resetNamespaceCtx, setNamespaceCtx } from '../ContextSaver';
import { createChildrenByDiff } from '../diff/nodeDiffComparator';
import { popCurrentRoot, pushCurrentRoot } from '../RootStack';
import { listenSimulatedDelegatedEvents } from '../../event/EventBinding';
export function bubbleRender(processing: VNode) {
resetNamespaceCtx(processing);
listenSimulatedDelegatedEvents(processing);
popCurrentRoot();
}

View File

@ -40,14 +40,20 @@ export class FlagUtils {
static removeFlag(node: VNode, flag: number) {
node.flags &= ~flag;
}
static removeLifecycleEffectFlags(node) {
node.flags &= ~LifecycleEffectArr;
}
static hasAnyFlag(node: VNode) {
// 有标志位
return node.flags !== InitFlag;
}
static hasFlag(node: VNode, flag) {
return (node.flags & flag) !== 0;
}
static setNoFlags(node: VNode) {
node.flags = InitFlag;
}
@ -55,6 +61,7 @@ export class FlagUtils {
static markAddition(node: VNode) {
node.flags |= Addition;
}
static setAddition(node: VNode) {
node.flags = Addition;
}

View File

@ -1,4 +1,7 @@
{
"name": "@cloudsop/horizon",
"description": "Horizon is a JavaScript framework library.",
"version": "0.0.51",
"private": true,
"workspaces": [
"libs/*"

View File

@ -0,0 +1,291 @@
/*
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
*
* openGauss is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import * as Horizon from '@cloudsop/horizon/index.ts';
describe('mouseenter和mouseleave事件测试', () => {
let container;
beforeEach(() => {
jest.resetModules();
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
it('在iframe中mouseleave事件的relateTarget属性', () => {
const iframe = document.createElement('iframe');
container.appendChild(iframe);
const iframeDocument = iframe.contentDocument;
iframeDocument.write(
'<!DOCTYPE html><html><head></head><body><div></div></body></html>',
);
iframeDocument.close();
const leaveEvents = [];
const node = Horizon.render(
<div
onMouseLeave={e => {
e.persist();
leaveEvents.push(e);
}}
/>,
iframeDocument.body.getElementsByTagName('div')[0],
);
node.dispatchEvent(
new MouseEvent('mouseout', {
bubbles: true,
cancelable: true,
relatedTarget: iframe.contentWindow,
}),
);
expect(leaveEvents.length).toBe(1);
expect(leaveEvents[0].target).toBe(node);
expect(leaveEvents[0].relatedTarget).toBe(iframe.contentWindow);
});
it('在iframe中mouseenter事件的relateTarget属性', () => {
const iframe = document.createElement('iframe');
container.appendChild(iframe);
const iframeDocument = iframe.contentDocument;
iframeDocument.write(
'<!DOCTYPE html><html><head></head><body><div></div></body></html>',
);
iframeDocument.close();
const enterEvents = [];
const node = Horizon.render(
<div
onMouseEnter={e => {
e.persist();
enterEvents.push(e);
}}
/>,
iframeDocument.body.getElementsByTagName('div')[0],
);
node.dispatchEvent(
new MouseEvent('mouseover', {
bubbles: true,
cancelable: true,
relatedTarget: null,
}),
);
expect(enterEvents.length).toBe(1);
expect(enterEvents[0].target).toBe(node);
expect(enterEvents[0].relatedTarget).toBe(iframe.contentWindow);
});
it('从新渲染的子组件触发mouseout事件子组件响应mouseenter事件父节点不响应', () => {
let parentEnterCalls = 0;
let childEnterCalls = 0;
let parent = null;
class Parent extends Horizon.Component {
render() {
return (
<div
onMouseEnter={() => parentEnterCalls++}
ref={node => (parent = node)}>
{this.props.showChild && (
<div onMouseEnter={() => childEnterCalls++}/>
)}
</div>
);
}
}
Horizon.render(<Parent/>, container);
Horizon.render(<Parent showChild={true}/>, container);
parent.dispatchEvent(
new MouseEvent('mouseout', {
bubbles: true,
cancelable: true,
relatedTarget: parent.firstChild,
}),
);
expect(childEnterCalls).toBe(1);
expect(parentEnterCalls).toBe(0);
});
it('render一个新组件兄弟节点触发mouseout事件mouseenter事件响应一次', done => {
const mockFn1 = jest.fn();
const mockFn2 = jest.fn();
const mockFn3 = jest.fn();
class Parent extends Horizon.Component {
constructor(props) {
super(props);
this.parentEl = Horizon.createRef();
}
componentDidMount() {
Horizon.render(<MouseEnterDetect/>, this.parentEl.current);
}
render() {
return <div ref={this.parentEl} id="parent" onMouseLeave={mockFn3}/>;
}
}
class MouseEnterDetect extends Horizon.Component {
constructor(props) {
super(props);
this.firstEl = Horizon.createRef();
this.siblingEl = Horizon.createRef();
}
componentDidMount() {
this.siblingEl.current.dispatchEvent(
new MouseEvent('mouseout', {
bubbles: true,
cancelable: true,
relatedTarget: this.firstEl.current,
}),
);
expect(mockFn1.mock.calls.length).toBe(1);
expect(mockFn2.mock.calls.length).toBe(1);
expect(mockFn3.mock.calls.length).toBe(0);
done();
}
render() {
return (
<Horizon.Fragment>
<div ref={this.firstEl} id="first" onMouseEnter={mockFn1}/>
<div ref={this.siblingEl} id="sibling" onMouseLeave={mockFn2}/>
</Horizon.Fragment>
);
}
}
Horizon.render(<Parent/>, container);
});
it('未被horizon管理的节点触发mouseout事件mouseenter事件也能正常触发', done => {
const mockFn = jest.fn();
class Parent extends Horizon.Component {
constructor(props) {
super(props);
this.parentEl = Horizon.createRef();
}
componentDidMount() {
Horizon.render(<MouseEnterDetect/>, this.parentEl.current);
}
render() {
return <div ref={this.parentEl}/>;
}
}
class MouseEnterDetect extends Horizon.Component {
constructor(props) {
super(props);
this.divRef = Horizon.createRef();
this.siblingEl = Horizon.createRef();
}
componentDidMount() {
const attachedNode = document.createElement('div');
this.divRef.current.appendChild(attachedNode);
attachedNode.dispatchEvent(
new MouseEvent('mouseout', {
bubbles: true,
cancelable: true,
relatedTarget: this.siblingEl.current,
}),
);
expect(mockFn.mock.calls.length).toBe(1);
done();
}
render() {
return (
<div ref={this.divRef}>
<div ref={this.siblingEl} onMouseEnter={mockFn}/>
</div>
);
}
}
Horizon.render(<Parent/>, container);
});
it('外部portal节点触发的mouseout事件根节点的mouseleave事件也能响应', () => {
const divRef = Horizon.createRef();
const onMouseLeave = jest.fn();
function Component() {
return (
<div onMouseLeave={onMouseLeave} id="parent">
{Horizon.createPortal(<div ref={divRef} id="sub"/>, document.body)}
</div>
);
}
Horizon.render(<Component/>, container);
divRef.current.dispatchEvent(
new MouseEvent('mouseout', {
bubbles: true,
cancelable: true,
relatedTarget: document.body,
}),
);
expect(onMouseLeave).toHaveBeenCalledTimes(1);
});
it('外部portal节点触发的mouseout事件根节点的mouseEnter事件也能响应', () => {
const divRef = Horizon.createRef();
const otherDivRef = Horizon.createRef();
const onMouseEnter = jest.fn();
function Component() {
return (
<div ref={divRef}>
{Horizon.createPortal(
<div ref={otherDivRef} onMouseEnter={onMouseEnter}/>,
document.body,
)}
</div>
);
}
Horizon.render(<Component/>, container);
divRef.current.dispatchEvent(
new MouseEvent('mouseout', {
bubbles: true,
cancelable: true,
relatedTarget: otherDivRef.current,
}),
);
expect(onMouseEnter).toHaveBeenCalledTimes(1);
});
});