Match-id-eb7f82b390c5406b3d957108c9dc9e8f650a0a74

This commit is contained in:
* 2022-06-22 16:47:15 +08:00 committed by *
parent 423b0374b3
commit 4d24ebd544
12 changed files with 181 additions and 126 deletions

View File

@ -1,4 +1,4 @@
import { allDelegatedHorizonEvents } from '../../event/EventCollection';
import { allDelegatedHorizonEvents } from '../../event/EventHub';
import { updateCommonProp } from './UpdateCommonProp';
import { setStyles } from './StyleHandler';
import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding';
@ -19,7 +19,6 @@ export function setDomProps(dom: Element, props: Object, isNativeTag: boolean, i
setStyles(dom, propVal);
} else if (isEventProp(propName)) {
// 事件监听属性处理
// TODO
const currentRoot = getCurrentRoot();
if (!allDelegatedHorizonEvents.has(propName)) {
listenNonDelegatedEvent(propName, dom, propVal);

View File

@ -1,27 +1,29 @@
/**
*
*/
import { allDelegatedHorizonEvents, allDelegatedNativeEvents } from './EventCollection';
import {isDocument} from '../dom/utils/Common';
import {
getNearestVNode,
getNonDelegatedListenerMap,
} from '../dom/DOMInternalKeys';
import {runDiscreteUpdates} from '../renderer/TreeBuilder';
import {isMounted} from '../renderer/vnode/VNodeUtils';
import {SuspenseComponent} from '../renderer/vnode/VNodeTags';
import {handleEventMain} from './HorizonEventMain';
import {decorateNativeEvent} from './customEvents/EventFactory';
allDelegatedHorizonEvents,
allDelegatedNativeEvents,
} from './EventHub';
import { isDocument } from '../dom/utils/Common';
import { getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys';
import { runDiscreteUpdates } from '../renderer/TreeBuilder';
import { handleEventMain } from './HorizonEventMain';
import { decorateNativeEvent } from './EventWrapper';
import { VNode } from '../renderer/vnode/VNode';
const listeningMarker = '_horizonListening' + Math.random().toString(36).slice(4);
const listeningMarker =
'_horizonListening' +
Math.random()
.toString(36)
.slice(4);
// 触发委托事件
function triggerDelegatedEvent(
nativeEvtName: string,
isCapture: boolean,
targetDom: EventTarget,
nativeEvent, // 事件对象event
nativeEvent // 事件对象event
) {
// 执行之前的调度事件
runDiscreteUpdates();
@ -33,11 +35,7 @@ function triggerDelegatedEvent(
}
// 监听委托事件
function listenToNativeEvent(
nativeEvtName: string,
delegatedElement: Element,
isCapture: boolean,
): void {
function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, isCapture: boolean): void {
let dom: Element | Document = delegatedElement;
// document层次可能触发selectionchange事件为了捕获这类事件selectionchange事件绑定在document节点上
if (nativeEvtName === 'selectionchange' && !isDocument(delegatedElement)) {
@ -70,10 +68,15 @@ export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) {
const isCapture = isCaptureEvent(eventName);
const nativeEvents = allDelegatedHorizonEvents.get(eventName);
nativeEvents.forEach(nativeEvents => {
listenToNativeEvent(nativeEvents, currentRoot.realNode, isCapture);
nativeEvents.forEach(nativeEvent => {
if (!currentRoot.delegatedNativeEvents.has(nativeEvent)) {
listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture);
currentRoot.delegatedNativeEvents.add(nativeEvent);
}
});
}
// 通过horizon事件名获取到native事件名
function getNativeEvtName(horizonEventName, capture) {
let nativeName;
@ -105,11 +108,7 @@ function getWrapperListener(horizonEventName, nativeEvtName, targetElement, list
}
// 非委托事件单独监听到各自dom节点
export function listenNonDelegatedEvent(
horizonEventName: string,
domElement: Element,
listener,
): void {
export function listenNonDelegatedEvent(horizonEventName: string, domElement: Element, listener): void {
const isCapture = isCaptureEvent(horizonEventName);
const nativeEvtName = getNativeEvtName(horizonEventName, isCapture);

View File

@ -1,15 +0,0 @@
import {horizonEventToNativeMap} from './const';
// 需要委托的horizon事件和原生事件对应关系
export const allDelegatedHorizonEvents = new Map();
// 所有委托的原生事件集合
export const allDelegatedNativeEvents = new Set();
horizonEventToNativeMap.forEach((dependencies, horizonEvent) => {
allDelegatedHorizonEvents.set(horizonEvent, dependencies);
allDelegatedHorizonEvents.set(horizonEvent + 'Capture', dependencies);
dependencies.forEach(d => {
allDelegatedNativeEvents.add(d);
});
});

View File

@ -1,3 +1,7 @@
// 需要委托的horizon事件和原生事件对应关系
export const allDelegatedHorizonEvents = new Map();
// 所有委托的原生事件集合
export const allDelegatedNativeEvents = new Set();
// Horizon事件和原生事件对应关系
export const horizonEventToNativeMap = new Map([
@ -33,10 +37,9 @@ export const horizonEventToNativeMap = new Map([
['onAnimationEnd', ['animationend']],
['onAnimationIteration', ['animationiteration']],
['onAnimationStart', ['animationstart']],
['onTransitionEnd', ['transitionend']]
['onTransitionEnd', ['transitionend']],
]);
export const CommonEventToHorizonMap = {
export const NativeEventToHorizonMap = {
click: 'click',
dblclick: 'doubleClick',
contextmenu: 'contextMenu',
@ -69,11 +72,22 @@ export const CommonEventToHorizonMap = {
compositionend: 'compositionEnd',
compositionupdate: 'compositionUpdate',
};
export const CHAR_CODE_SPACE = 32;
export const EVENT_TYPE_BUBBLE = 'Bubble';
export const EVENT_TYPE_CAPTURE = 'Capture';
export const EVENT_TYPE_ALL = 'All';
horizonEventToNativeMap.forEach((dependencies, horizonEvent) => {
allDelegatedHorizonEvents.set(horizonEvent, dependencies);
allDelegatedHorizonEvents.set(horizonEvent + 'Capture', dependencies);
dependencies.forEach(d => {
allDelegatedNativeEvents.add(d);
});
});
export function transformToHorizonEvent(nativeEvtName: string) {
const name = NativeEventToHorizonMap[nativeEvtName];
// 例dragEnd -> onDragEnd
return !name ? '' : `on${name[0].toUpperCase()}${name.slice(1)}`;
}

View File

@ -1,19 +1,76 @@
import type { AnyNativeEvent } from './Types';
import { ListenerUnitList } from './Types';
import { AnyNativeEvent, ListenerUnitList } from './Types';
import type { VNode } from '../renderer/Types';
import { CommonEventToHorizonMap, EVENT_TYPE_BUBBLE, EVENT_TYPE_CAPTURE, horizonEventToNativeMap } from './const';
import { getListeners as getChangeListeners } from './simulatedEvtHandler/ChangeEventHandler';
import { setPropertyWritable } from './utils';
import { decorateNativeEvent } from './customEvents/EventFactory';
import { isInputElement, setPropertyWritable } from './utils';
import { decorateNativeEvent } from './EventWrapper';
import { getListenersFromTree } from './ListenerGetter';
import { asyncUpdates, runDiscreteUpdates } from '../renderer/Renderer';
import { findRoot } from '../renderer/vnode/VNodeUtils';
import { syncRadiosHandler } from '../dom/valueHandler/InputValueHandler';
import {
EVENT_TYPE_ALL,
EVENT_TYPE_BUBBLE,
EVENT_TYPE_CAPTURE,
horizonEventToNativeMap,
transformToHorizonEvent,
} from './EventHub';
import { getDomTag } from '../dom/utils/Common';
import { updateInputValueIfChanged } from '../dom/valueHandler/ValueChangeHandler';
import { getDom } from '../dom/DOMInternalKeys';
// web规范鼠标右键key值
const RIGHT_MOUSE_BUTTON = 2;
// 返回是否需要触发change事件标记
// | 元素 | 事件 | 需要值变更 |
// | --- | --- | --------------- |
// | <select/> / <input type="file/> | change | NO |
// | <input type="checkbox" /> <input type="radio" /> | click | YES |
// | <input type="input /> / <input type="text" /> | input / change | YES |
function shouldTriggerChangeEvent(targetDom, evtName) {
const { type } = targetDom;
const domTag = getDomTag(targetDom);
if (domTag === 'select' || (domTag === 'input' && type === 'file')) {
return evtName === 'change';
} else if (domTag === 'input' && (type === 'checkbox' || type === 'radio')) {
if (evtName === 'click') {
return updateInputValueIfChanged(targetDom);
}
} else if (isInputElement(targetDom)) {
if (evtName === 'input' || evtName === 'change') {
return updateInputValueIfChanged(targetDom);
}
}
return false;
}
/**
*
* input/textarea/select的onChange事件
*/
function getChangeListeners(
nativeEvtName: string,
nativeEvt: AnyNativeEvent,
vNode: null | VNode,
): ListenerUnitList {
if (!vNode) {
return [];
}
const targetDom = getDom(vNode);
// 判断是否需要触发change事件
if (shouldTriggerChangeEvent(targetDom, nativeEvtName)) {
const event = decorateNativeEvent(
'onChange',
'change',
nativeEvt,
);
return getListenersFromTree(vNode, 'onChange', event, EVENT_TYPE_ALL);
}
return [];
}
// 获取事件触发的普通事件监听方法队列
function getCommonListeners(
nativeEvtName: string,
@ -22,8 +79,7 @@ function getCommonListeners(
target: null | EventTarget,
isCapture: boolean,
): ListenerUnitList {
const name = CommonEventToHorizonMap[nativeEvtName];
const horizonEvtName = !name ? '' : `on${name[0].toUpperCase()}${name.slice(1)}`; // 例dragEnd -> onDragEnd
const horizonEvtName = transformToHorizonEvent(nativeEvtName);
if (!horizonEvtName) {
return [];

View File

@ -1,7 +1,7 @@
import { VNode } from '../renderer/Types';
import { DomComponent } from '../renderer/vnode/VNodeTags';
import { EVENT_TYPE_ALL, EVENT_TYPE_CAPTURE, EVENT_TYPE_BUBBLE } from './const';
import { AnyNativeEvent, ListenerUnitList } from './Types';
import { EVENT_TYPE_ALL, EVENT_TYPE_BUBBLE, EVENT_TYPE_CAPTURE } from './EventHub';
// 从vnode属性中获取事件listener
function getListenerFromVNode(vNode: VNode, eventName: string): Function | null {

View File

@ -1,62 +0,0 @@
import {decorateNativeEvent} from '../customEvents/EventFactory';
import {getDom} from '../../dom/DOMInternalKeys';
import {updateInputValueIfChanged} from '../../dom/valueHandler/ValueChangeHandler';
import {isInputElement} from '../utils';
import {EVENT_TYPE_ALL} from '../const';
import {AnyNativeEvent, ListenerUnitList} from '../Types';
import {
getListenersFromTree,
} from '../ListenerGetter';
import {VNode} from '../../renderer/Types';
import {getDomTag} from '../../dom/utils/Common';
// 返回是否需要触发change事件标记
// | 元素 | 事件 | 需要值变更 |
// | --- | --- | --------------- |
// | <select/> / <input type="file/> | change | NO |
// | <input type="checkbox" /> <input type="radio" /> | click | YES |
// | <input type="input /> / <input type="text" /> | input / change | YES |
function shouldTriggerChangeEvent(targetDom, evtName) {
const { type } = targetDom;
const domTag = getDomTag(targetDom);
if (domTag === 'select' || (domTag === 'input' && type === 'file')) {
return evtName === 'change';
} else if (domTag === 'input' && (type === 'checkbox' || type === 'radio')) {
if (evtName === 'click') {
return updateInputValueIfChanged(targetDom);
}
} else if (isInputElement(targetDom)) {
if (evtName === 'input' || evtName === 'change') {
return updateInputValueIfChanged(targetDom);
}
}
return false;
}
/**
*
* input/textarea/select的onChange事件
*/
export function getListeners(
nativeEvtName: string,
nativeEvt: AnyNativeEvent,
vNode: null | VNode
): ListenerUnitList {
if (!vNode) {
return [];
}
const targetDom = getDom(vNode);
// 判断是否需要触发change事件
if (shouldTriggerChangeEvent(targetDom, nativeEvtName)) {
const event = decorateNativeEvent(
'onChange',
'change',
nativeEvt,
);
return getListenersFromTree(vNode, 'onChange', event, EVENT_TYPE_ALL);
}
return [];
}

View File

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

View File

@ -1,6 +1,13 @@
import * as Horizon from '@cloudsop/horizon/index.ts';
import * as TestUtils from '../jest/testUtils';
function dispatchChangeEvent(input) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeInputValueSetter.call(input, 'test');
input.dispatchEvent(new Event('input', { bubbles: true }));
}
describe('事件', () => {
const LogUtils = TestUtils.getLogUtils();
it('根节点挂载全量事件', () => {
@ -162,11 +169,7 @@ describe('事件', () => {
LogUtils.log('change');
},
});
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeInputValueSetter.call(inputRef.current, 'test');
inputRef.current.dispatchEvent(new Event('input', { bubbles: true }));
dispatchChangeEvent(inputRef.current);
expect(LogUtils.getAndClear()).toEqual(['change']);
});
@ -209,4 +212,63 @@ describe('事件', () => {
// 先选择选项1radio1应该重新触发onchange
clickRadioAndExpect(radio1Ref.current, [2, 1]);
});
it('多根节点下,事件挂载正确', () => {
const root1 = document.createElement('div');
const root2 = document.createElement('div');
root1.key = 'root1';
root2.key = 'root2';
let input1, input2, update1, update2;
function App1() {
const [props, setProps] = Horizon.useState({});
update1 = setProps;
return (
<input
{...props}
ref={n => (input1 = n)}
onChange={() => {
LogUtils.log('input1 changed');
}}
/>
);
}
function App2() {
const [props, setProps] = Horizon.useState({});
update2 = setProps;
return (
<input
{...props}
ref={n => (input2 = n)}
onChange={() => {
LogUtils.log('input2 changed');
}}
/>
);
}
// 多根mount阶段挂载onChange事件
Horizon.render(<App1 key={1} />, root1);
Horizon.render(<App2 key={2} />, root2);
dispatchChangeEvent(input1);
expect(LogUtils.getAndClear()).toEqual(['input1 changed']);
dispatchChangeEvent(input2);
expect(LogUtils.getAndClear()).toEqual(['input2 changed']);
// 多根update阶段挂载onClick事件
update1({
onClick: () => LogUtils.log('input1 clicked'),
});
update2({
onClick: () => LogUtils.log('input2 clicked'),
});
input1.click();
expect(LogUtils.getAndClear()).toEqual(['input1 clicked']);
input2.click();
expect(LogUtils.getAndClear()).toEqual(['input2 clicked']);
});
});

View File

@ -1,4 +1,4 @@
import { allDelegatedNativeEvents } from '../../../libs/horizon/src/event/EventCollection';
import { allDelegatedNativeEvents } from '@cloudsop/horizon/src/event/EventHub';
//import * as LogUtils from './logUtils';
export const stopBubbleOrCapture = (e, value) => {
@ -107,4 +107,4 @@ export function getLogUtils() {
logger = new LogUtils();
}
return logger;
}
}