diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts index 0d12dd84..7864426d 100644 --- a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts +++ b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts @@ -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); diff --git a/libs/horizon/src/event/EventBinding.ts b/libs/horizon/src/event/EventBinding.ts index 73c43c35..9398d648 100644 --- a/libs/horizon/src/event/EventBinding.ts +++ b/libs/horizon/src/event/EventBinding.ts @@ -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); diff --git a/libs/horizon/src/event/EventCollection.ts b/libs/horizon/src/event/EventCollection.ts deleted file mode 100644 index d70dcf10..00000000 --- a/libs/horizon/src/event/EventCollection.ts +++ /dev/null @@ -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); - }); -}); diff --git a/libs/horizon/src/event/const.ts b/libs/horizon/src/event/EventHub.ts similarity index 73% rename from libs/horizon/src/event/const.ts rename to libs/horizon/src/event/EventHub.ts index 87d10821..944112f2 100644 --- a/libs/horizon/src/event/const.ts +++ b/libs/horizon/src/event/EventHub.ts @@ -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)}`; +} diff --git a/libs/horizon/src/event/customEvents/EventFactory.ts b/libs/horizon/src/event/EventWrapper.ts similarity index 100% rename from libs/horizon/src/event/customEvents/EventFactory.ts rename to libs/horizon/src/event/EventWrapper.ts diff --git a/libs/horizon/src/event/HorizonEventMain.ts b/libs/horizon/src/event/HorizonEventMain.ts index e183ce2f..aff68d17 100644 --- a/libs/horizon/src/event/HorizonEventMain.ts +++ b/libs/horizon/src/event/HorizonEventMain.ts @@ -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事件标记 +// | 元素 | 事件 | 需要值变更 | +// | --- | --- | --------------- | +// | | click | YES | +// | | 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 []; diff --git a/libs/horizon/src/event/ListenerGetter.ts b/libs/horizon/src/event/ListenerGetter.ts index d6860f35..731368ed 100644 --- a/libs/horizon/src/event/ListenerGetter.ts +++ b/libs/horizon/src/event/ListenerGetter.ts @@ -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 { diff --git a/libs/horizon/src/event/WrapperListener.ts b/libs/horizon/src/event/WrapperListener.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts b/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts deleted file mode 100644 index 285ac435..00000000 --- a/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts +++ /dev/null @@ -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事件标记 -// | 元素 | 事件 | 需要值变更 | -// | --- | --- | --------------- | -// | | click | YES | -// | | 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 []; -} diff --git a/libs/horizon/src/renderer/vnode/VNode.ts b/libs/horizon/src/renderer/vnode/VNode.ts index d67f0e73..7c360e47 100644 --- a/libs/horizon/src/renderer/vnode/VNode.ts +++ b/libs/horizon/src/renderer/vnode/VNode.ts @@ -78,6 +78,7 @@ export class VNode { // 根节点数据 toUpdateNodes: Set | null; // 保存要更新的节点 delegatedEvents: Set + delegatedNativeEvents: Set belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode,处理ref的时候使用 @@ -98,6 +99,7 @@ export class VNode { this.task = null; this.toUpdateNodes = new Set(); this.delegatedEvents = new Set(); + this.delegatedNativeEvents = new Set(); this.updates = null; this.stateCallbacks = null; this.state = null; diff --git a/scripts/__tests__/EventTest/EventMain.test.js b/scripts/__tests__/EventTest/EventMain.test.js index 1e6c52b7..2b3868ba 100644 --- a/scripts/__tests__/EventTest/EventMain.test.js +++ b/scripts/__tests__/EventTest/EventMain.test.js @@ -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('事件', () => { // 先选择选项1,radio1应该重新触发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 ( + (input1 = n)} + onChange={() => { + LogUtils.log('input1 changed'); + }} + /> + ); + } + + function App2() { + const [props, setProps] = Horizon.useState({}); + update2 = setProps; + + return ( + (input2 = n)} + onChange={() => { + LogUtils.log('input2 changed'); + }} + /> + ); + } + + // 多根mount阶段挂载onChange事件 + Horizon.render(, root1); + Horizon.render(, 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']); + }); }); diff --git a/scripts/__tests__/jest/testUtils.js b/scripts/__tests__/jest/testUtils.js index 6d4b7ac2..65cea65e 100755 --- a/scripts/__tests__/jest/testUtils.js +++ b/scripts/__tests__/jest/testUtils.js @@ -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; -} \ No newline at end of file +}