diff --git a/libs/horizon/src/dom/DOMOperator.ts b/libs/horizon/src/dom/DOMOperator.ts index 073456fc..07700dd4 100644 --- a/libs/horizon/src/dom/DOMOperator.ts +++ b/libs/horizon/src/dom/DOMOperator.ts @@ -26,7 +26,7 @@ import { watchValueChange } from './valueHandler/ValueChangeHandler'; import { DomComponent, DomText } from '../renderer/vnode/VNodeTags'; import { updateCommonProp } from './DOMPropertiesHandler/UpdateCommonProp'; -export type Props = { +export type Props = Record & { autoFocus?: boolean; children?: any; dangerouslySetInnerHTML?: any; diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts index 1ac626a0..0d12dd84 100644 --- a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts +++ b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts @@ -1,22 +1,12 @@ -import { - allDelegatedHorizonEvents, -} from '../../event/EventCollection'; +import { allDelegatedHorizonEvents } from '../../event/EventCollection'; import { updateCommonProp } from './UpdateCommonProp'; import { setStyles } from './StyleHandler'; -import { - lazyDelegateOnRoot, - listenNonDelegatedEvent, -} from '../../event/EventBinding'; +import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding'; import { isEventProp } from '../validators/ValidateProps'; import { getCurrentRoot } from '../../renderer/TreeBuilder'; // 初始化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 { const keysOfProps = Object.keys(props); let propName; let propVal; @@ -36,7 +26,8 @@ export function setDomProps( } else if (currentRoot && !currentRoot.delegatedEvents.has(propName)) { lazyDelegateOnRoot(currentRoot, propName); } - } else if (propName === 'children') { // 只处理纯文本子节点,其他children在VNode树中处理 + } else if (propName === 'children') { + // 只处理纯文本子节点,其他children在VNode树中处理 const type = typeof propVal; if (type === 'string' || type === 'number') { dom.textContent = propVal; @@ -50,10 +41,7 @@ export function setDomProps( } // 找出两个 DOM 属性的差别,生成需要更新的属性集合 -export function compareProps( - oldProps: Object, - newProps: Object, -): Object { +export function compareProps(oldProps: Object, newProps: Object): Object { let updatesForStyle = {}; const toUpdateProps = {}; const keysOfOldProps = Object.keys(oldProps); @@ -113,7 +101,8 @@ export function compareProps( } if (propName === 'style') { - if (oldPropValue) { // 之前 style 属性有设置非空值 + if (oldPropValue) { + // 之前 style 属性有设置非空值 // 原来有这个 style,但现在没这个 style 了 oldStyleProps = Object.keys(oldPropValue); for (let j = 0; j < oldStyleProps.length; j++) { @@ -131,7 +120,8 @@ export function compareProps( updatesForStyle[styleProp] = newPropValue[styleProp]; } } - } else { // 之前未设置 style 属性或者设置了空值 + } else { + // 之前未设置 style 属性或者设置了空值 if (Object.keys(updatesForStyle).length === 0) { toUpdateProps[propName] = null; } @@ -150,10 +140,12 @@ export function compareProps( toUpdateProps[propName] = String(newPropValue); } } else if (isEventProp(propName)) { + const currentRoot = getCurrentRoot(); if (!allDelegatedHorizonEvents.has(propName)) { toUpdateProps[propName] = newPropValue; + } else if (currentRoot && !currentRoot.delegatedEvents.has(propName)) { + lazyDelegateOnRoot(currentRoot, propName); } - // TODO } else { toUpdateProps[propName] = newPropValue; } diff --git a/libs/horizon/src/dom/utils/Common.ts b/libs/horizon/src/dom/utils/Common.ts index dae06d2b..3ed6a462 100644 --- a/libs/horizon/src/dom/utils/Common.ts +++ b/libs/horizon/src/dom/utils/Common.ts @@ -56,6 +56,10 @@ export function getDomTag(dom) { return dom.nodeName.toLowerCase(); } +export function isInputElement(dom: Element): dom is HTMLInputElement { + return getDomTag(dom) === 'input'; +} + const types = ['button', 'input', 'select', 'textarea']; // button、input、select、textarea、如果有 autoFocus 属性需要focus diff --git a/libs/horizon/src/dom/valueHandler/InputValueHandler.ts b/libs/horizon/src/dom/valueHandler/InputValueHandler.ts index 368bf288..2ebe01a7 100644 --- a/libs/horizon/src/dom/valueHandler/InputValueHandler.ts +++ b/libs/horizon/src/dom/valueHandler/InputValueHandler.ts @@ -1,20 +1,21 @@ -import {updateCommonProp} from '../DOMPropertiesHandler/UpdateCommonProp'; -import {getVNodeProps} from '../DOMInternalKeys'; -import {IProperty} from '../utils/Interface'; -import {isInputValueChanged} from './ValueChangeHandler'; +import { updateCommonProp } from '../DOMPropertiesHandler/UpdateCommonProp'; +import { IProperty } from '../utils/Interface'; +import { isInputElement } from '../utils/Common'; +import { getVNodeProps } from '../DOMInternalKeys'; +import { updateInputValueIfChanged } from './ValueChangeHandler'; function getInitValue(dom: HTMLInputElement, properties: IProperty) { - const {value, defaultValue, checked, defaultChecked} = properties; + const { value, defaultValue, checked, defaultChecked } = properties; const defaultValueStr = defaultValue != null ? defaultValue : ''; const initValue = value != null ? value : defaultValueStr; const initChecked = checked != null ? checked : defaultChecked; - return {initValue, initChecked}; + return { initValue, initChecked }; } export function getInputPropsWithoutValue(dom: HTMLInputElement, properties: IProperty) { - // checked属于必填属性,无法置空 + // checked属于必填属性,无法置 let {checked} = properties; if (checked == null) { checked = getInitValue(dom, properties).initChecked; @@ -59,30 +60,26 @@ export function setInitInputValue(dom: HTMLInputElement, properties: IProperty) dom.defaultChecked = Boolean(initChecked); } -export function resetInputValue(dom: HTMLInputElement, properties: IProperty) { - const {name, type} = properties; - // 如果是 radio,先更新相同 name 的 radio - if (type === 'radio' && name != null) { - const radioList = document.querySelectorAll(`input[type="radio"][name="${name}"]`); +// 找出同一form内,name相同的Radio,更新它们Handler的Value +export function syncRadiosHandler(targetRadio: Element) { + if (isInputElement(targetRadio)) { + const props = getVNodeProps(targetRadio); + if (props) { + const { name, type } = props; + if (type === 'radio' && name != null) { + const radioList = document.querySelectorAll(`input[type="radio"][name="${name}"]`); + for (let i = 0; i < radioList.length; i++) { + const radio = radioList[i]; + if (radio === targetRadio) { + continue; + } + if (radio.form != null && targetRadio.form != null && radio.form !== targetRadio.form) { + continue; + } - for (let i = 0; i < radioList.length; i++) { - const radio = radioList[i]; - if (radio === dom) { - continue; + updateInputValueIfChanged(radio); + } } - // @ts-ignore - if (radio.form !== dom.form) { - continue; - } - - // @ts-ignore - const nonHorizonRadioProps = getVNodeProps(radio); - - isInputValueChanged(radio); - // @ts-ignore - updateInputValue(radio, nonHorizonRadioProps); } - } else { - updateInputValue(dom, properties); } } diff --git a/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts b/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts index 34406ef8..207e8c2e 100644 --- a/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts +++ b/libs/horizon/src/dom/valueHandler/ValueChangeHandler.ts @@ -54,7 +54,7 @@ export function watchValueChange(dom) { } } -export function isInputValueChanged(dom) { +export function updateInputValueIfChanged(dom) { const handler = dom[HANDLER_KEY]; if (!handler) { return true; diff --git a/libs/horizon/src/dom/valueHandler/index.ts b/libs/horizon/src/dom/valueHandler/index.ts index 2e5b6da7..5948a937 100644 --- a/libs/horizon/src/dom/valueHandler/index.ts +++ b/libs/horizon/src/dom/valueHandler/index.ts @@ -8,7 +8,6 @@ import { getInputPropsWithoutValue, setInitInputValue, updateInputValue, - resetInputValue, } from './InputValueHandler'; import { getOptionPropsWithoutValue, @@ -21,7 +20,6 @@ import { getTextareaPropsWithoutValue, updateTextareaValue, } from './TextareaValueHandler'; -import {getDomTag} from '../utils/Common'; // 获取元素除了被代理的值以外的属性 function getPropsWithoutValue(type: string, dom: HorizonDom, properties: IProperty) { @@ -73,26 +71,8 @@ function updateValue(type: string, dom: HorizonDom, properties: IProperty) { } } -function resetValue(dom: HorizonDom, properties: IProperty) { - const type = getDomTag(dom); - switch (type) { - case 'input': - resetInputValue(dom, properties); - break; - case 'select': - updateSelectValue(dom, properties); - break; - case 'textarea': - updateTextareaValue(dom, properties); - break; - default: - break; - } -} - export { getPropsWithoutValue, setInitValue, updateValue, - resetValue, }; diff --git a/libs/horizon/src/event/ControlledValueUpdater.ts b/libs/horizon/src/event/ControlledValueUpdater.ts deleted file mode 100644 index 5e565a1d..00000000 --- a/libs/horizon/src/event/ControlledValueUpdater.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {getVNodeProps} from '../dom/DOMInternalKeys'; -import {resetValue} from '../dom/valueHandler'; - -let updateList: Array | null = null; - -// 受控组件值重新赋值 -function updateValue(target: Element) { - const props = getVNodeProps(target); - if (props) { - resetValue(target, props); - } -} - -// 存储队列中缓存组件 -export function addValueUpdateList(target: EventTarget): void { - if (updateList) { - updateList.push(target); - } else { - updateList = [target]; - } -} - -// 判断是否需要重新赋值 -export function shouldUpdateValue(): boolean { - return updateList !== null && updateList.length > 0; -} - -// 从缓存队列中对受控组件进行赋值 -export function updateControlledValue() { - if (!updateList) { - return; - } - updateList.forEach(item => { - updateValue(item); - }); - updateList = null; -} diff --git a/libs/horizon/src/event/HorizonEventMain.ts b/libs/horizon/src/event/HorizonEventMain.ts index 35db487b..e183ce2f 100644 --- a/libs/horizon/src/event/HorizonEventMain.ts +++ b/libs/horizon/src/event/HorizonEventMain.ts @@ -1,22 +1,15 @@ import type { AnyNativeEvent } from './Types'; +import { ListenerUnitList } from './Types'; import type { VNode } from '../renderer/Types'; -import { - CommonEventToHorizonMap, - horizonEventToNativeMap, - EVENT_TYPE_BUBBLE, - EVENT_TYPE_CAPTURE, -} from './const'; +import { CommonEventToHorizonMap, EVENT_TYPE_BUBBLE, EVENT_TYPE_CAPTURE, horizonEventToNativeMap } from './const'; import { getListeners as getChangeListeners } from './simulatedEvtHandler/ChangeEventHandler'; -import { - setPropertyWritable, -} from './utils'; +import { setPropertyWritable } from './utils'; import { decorateNativeEvent } from './customEvents/EventFactory'; import { getListenersFromTree } from './ListenerGetter'; -import { shouldUpdateValue, updateControlledValue } from './ControlledValueUpdater'; import { asyncUpdates, runDiscreteUpdates } from '../renderer/Renderer'; import { findRoot } from '../renderer/vnode/VNodeUtils'; -import { ListenerUnitList } from './Types'; +import { syncRadiosHandler } from '../dom/valueHandler/InputValueHandler'; // web规范,鼠标右键key值 const RIGHT_MOUSE_BUTTON = 2; @@ -80,7 +73,6 @@ function getProcessListeners( target, isCapture: boolean, ): ListenerUnitList { - // TODO 重复从树中获取监听器 // 触发普通委托事件 let listenerList: ListenerUnitList = getCommonListeners( nativeEvtName, @@ -92,12 +84,11 @@ function getProcessListeners( // 触发特殊handler委托事件 if (!isCapture) { - if (horizonEventToNativeMap.get('onChange').includes(nativeEvtName)) { + if (horizonEventToNativeMap.get('onChange')!.includes(nativeEvtName)) { listenerList = listenerList.concat(getChangeListeners( nativeEvtName, nativeEvent, vNode, - target, )); } } @@ -110,7 +101,7 @@ function triggerHorizonEvents( isCapture: boolean, nativeEvent: AnyNativeEvent, vNode: VNode | null, -): void { +) { const nativeEventTarget = nativeEvent.target || nativeEvent.srcElement; // 获取委托事件队列 @@ -118,6 +109,8 @@ function triggerHorizonEvents( // 处理触发的事件队列 processListeners(listenerList); + + return listenerList; } @@ -148,13 +141,18 @@ export function handleEventMain( // 没有事件在执行,经过调度再执行事件 isInEventsExecution = true; + let shouldDispatchUpdate = false; try { - asyncUpdates(() => triggerHorizonEvents(nativeEvtName, isCapture, nativeEvent, startVNode)); + const listeners = asyncUpdates(() => triggerHorizonEvents(nativeEvtName, isCapture, nativeEvent, startVNode)); + if (listeners.length) { + shouldDispatchUpdate = true; + } } finally { isInEventsExecution = false; - if (shouldUpdateValue()) { + if (shouldDispatchUpdate) { runDiscreteUpdates(); - updateControlledValue(); + // 若是Radio,同步同组其他Radio的Handler Value + syncRadiosHandler(nativeEvent.target as Element); } } } diff --git a/libs/horizon/src/event/const.ts b/libs/horizon/src/event/const.ts index c0e185a4..87d10821 100644 --- a/libs/horizon/src/event/const.ts +++ b/libs/horizon/src/event/const.ts @@ -44,6 +44,7 @@ export const CommonEventToHorizonMap = { focusin: 'focus', focusout: 'blur', input: 'input', + select: 'select', keydown: 'keyDown', keypress: 'keyPress', keyup: 'keyUp', diff --git a/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts b/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts index f1b781af..285ac435 100644 --- a/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts +++ b/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts @@ -1,7 +1,6 @@ import {decorateNativeEvent} from '../customEvents/EventFactory'; import {getDom} from '../../dom/DOMInternalKeys'; -import {isInputValueChanged} from '../../dom/valueHandler/ValueChangeHandler'; -import {addValueUpdateList} from '../ControlledValueUpdater'; +import {updateInputValueIfChanged} from '../../dom/valueHandler/ValueChangeHandler'; import {isInputElement} from '../utils'; import {EVENT_TYPE_ALL} from '../const'; import {AnyNativeEvent, ListenerUnitList} from '../Types'; @@ -25,11 +24,11 @@ function shouldTriggerChangeEvent(targetDom, evtName) { return evtName === 'change'; } else if (domTag === 'input' && (type === 'checkbox' || type === 'radio')) { if (evtName === 'click') { - return isInputValueChanged(targetDom); + return updateInputValueIfChanged(targetDom); } } else if (isInputElement(targetDom)) { if (evtName === 'input' || evtName === 'change') { - return isInputValueChanged(targetDom); + return updateInputValueIfChanged(targetDom); } } return false; @@ -42,8 +41,7 @@ function shouldTriggerChangeEvent(targetDom, evtName) { export function getListeners( nativeEvtName: string, nativeEvt: AnyNativeEvent, - vNode: null | VNode, - target: null | EventTarget, + vNode: null | VNode ): ListenerUnitList { if (!vNode) { return []; @@ -52,7 +50,6 @@ export function getListeners( // 判断是否需要触发change事件 if (shouldTriggerChangeEvent(targetDom, nativeEvtName)) { - addValueUpdateList(target); const event = decorateNativeEvent( 'onChange', 'change', diff --git a/libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts b/libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts deleted file mode 100644 index cd5a09ad..00000000 --- a/libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts +++ /dev/null @@ -1,112 +0,0 @@ -import {decorateNativeEvent} from '../customEvents/EventFactory'; -import {shallowCompare} from '../../renderer/utils/compare'; -import {getFocusedDom} from '../../dom/utils/Common'; -import {getDom} from '../../dom/DOMInternalKeys'; -import {isDocument} from '../../dom/utils/Common'; -import {isInputElement, setPropertyWritable} from '../utils'; -import type {AnyNativeEvent} from '../Types'; -import {getListenersFromTree} from '../ListenerGetter'; -import type {VNode} from '../../renderer/Types'; -import {EVENT_TYPE_ALL} from '../const'; -import {ListenerUnitList} from '../Types'; - -const horizonEventName = 'onSelect'; - -let currentElement = null; -let currentVNode = null; -let lastSelection: Selection | null = null; - -function initTargetCache(dom, vNode) { - if (isInputElement(dom) || dom.contentEditable === 'true') { - currentElement = dom; - currentVNode = vNode; - lastSelection = null; - } -} - -function clearTargetCache() { - currentElement = null; - currentVNode = null; - lastSelection = null; -} - -// 标记是否在鼠标事件过程中 -let isInMouseEvent = false; - -// 获取节点所在的document对象 -function getDocument(eventTarget) { - if (eventTarget.window === eventTarget) { - return eventTarget.document; - } - if (isDocument(eventTarget)) { - return eventTarget; - } - return eventTarget.ownerDocument; -} - -function getSelectEvent(nativeEvent, target) { - const doc = getDocument(target); - if (isInMouseEvent || currentElement == null || currentElement !== getFocusedDom(doc)) { - return []; - } - - const currentSelection = window.getSelection(); - if (!shallowCompare(lastSelection, currentSelection)) { - lastSelection = currentSelection; - - const event = decorateNativeEvent( - horizonEventName, - 'select', - nativeEvent, - ); - setPropertyWritable(nativeEvent, 'target'); - event.target = currentElement; - - return getListenersFromTree( - currentVNode, - horizonEventName, - event, - EVENT_TYPE_ALL - ); - } - return []; -} - - -/** - * 该插件创建一个onSelect事件 - * 支持元素: input、textarea、contentEditable元素 - * 触发场景:用户输入、折叠选择、文本选择 - */ -export function getListeners( - nativeEvtName: string, - nativeEvt: AnyNativeEvent, - vNode: null | VNode, - target: null | EventTarget, -): ListenerUnitList { - const targetNode = vNode ? getDom(vNode) : window; - let eventUnitList: ListenerUnitList = []; - switch (nativeEvtName) { - case 'focusin': - initTargetCache(targetNode, vNode); - break; - case 'focusout': - clearTargetCache(); - break; - case 'mousedown': - isInMouseEvent = true; - break; - case 'contextmenu': - case 'mouseup': - case 'dragend': - isInMouseEvent = false; - eventUnitList = getSelectEvent(nativeEvt, target); - break; - case 'selectionchange': - case 'keydown': - case 'keyup': - eventUnitList = getSelectEvent(nativeEvt, target); - } - - return eventUnitList; -} diff --git a/package.json b/package.json index 58bf2f9f..c31b27cf 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "lint": "eslint . --ext .ts", "build": " rollup --config ./scripts/rollup/rollup.config.js", + "build:watch": " rollup --watch --config ./scripts/rollup/rollup.config.js", "build-3rdLib": "node ./scripts/gen3rdLib.js", "build-3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev", "build-horizon3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev --type horizon", diff --git a/scripts/__tests__/EventTest/EventMain.test.js b/scripts/__tests__/EventTest/EventMain.test.js index 4a75d46e..1e6c52b7 100644 --- a/scripts/__tests__/EventTest/EventMain.test.js +++ b/scripts/__tests__/EventTest/EventMain.test.js @@ -34,7 +34,7 @@ describe('事件', () => { 'btn capture', 'btn bubble', 'p bubble', - 'div bubble' + 'div bubble', ]); }); @@ -46,14 +46,14 @@ describe('事件', () => { keyCode = e.keyCode; }} />, - container, + container ); node.dispatchEvent( new KeyboardEvent('keypress', { keyCode: 65, bubbles: true, cancelable: true, - }), + }) ); expect(keyCode).toBe(65); }); @@ -64,7 +64,10 @@ describe('事件', () => { <>
LogUtils.log('div capture')} onClick={() => LogUtils.log('div bubble')}>

LogUtils.log('p capture')} onClick={() => LogUtils.log('p bubble')}> -

@@ -78,7 +81,7 @@ describe('事件', () => { 'div capture', 'p capture', 'btn capture', - 'btn bubble' + 'btn bubble', ]); }); @@ -86,7 +89,10 @@ describe('事件', () => { const App = () => { return ( <> -
TestUtils.stopBubbleOrCapture(e, 'div capture')} onClick={() => LogUtils.log('div bubble')}> +
TestUtils.stopBubbleOrCapture(e, 'div capture')} + onClick={() => LogUtils.log('div bubble')} + >

LogUtils.log('p capture')} onClick={() => LogUtils.log('p bubble')}>