From d14122a5d3ef55ba66dfbabb9d196eace17e58c2 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 20 Apr 2022 14:47:42 +0800 Subject: [PATCH 1/4] Match-id-c512ddd2eafdc01bfd6b07d73a3aead7828578e8 --- libs/horizon/src/dom/DOMInternalKeys.ts | 24 +++++-------- .../dom/DOMPropertiesHandler/StyleHandler.ts | 22 ++++++------ .../src/dom/validators/ValidateProps.ts | 2 +- libs/horizon/src/dom/valueHandler/index.ts | 2 +- libs/horizon/src/event/HorizonEventMain.ts | 17 +++++---- libs/horizon/src/renderer/vnode/VNodeUtils.ts | 35 ++++++------------- scripts/__tests__/DomTest/Attribute.test.js | 8 ++++- scripts/__tests__/DomTest/DomInput.test.js | 7 +++- 8 files changed, 54 insertions(+), 63 deletions(-) diff --git a/libs/horizon/src/dom/DOMInternalKeys.ts b/libs/horizon/src/dom/DOMInternalKeys.ts index 2f4ccb23..f389a0cf 100644 --- a/libs/horizon/src/dom/DOMInternalKeys.ts +++ b/libs/horizon/src/dom/DOMInternalKeys.ts @@ -49,23 +49,17 @@ export function getVNode(dom: Node|Container): VNode | null { // 用 DOM 对象,来寻找其对应或者说是最近父级的 vNode export function getNearestVNode(dom: Node): null | VNode { - let vNode = dom[INTERNAL_VNODE]; - if (vNode) { // 如果是已经被框架标记过的 DOM 节点,那么直接返回其 VNode 实例 - return vNode; + let domNode: Node | null = dom; + // 寻找当前节点及其所有祖先节点是否有标记VNODE + while (domNode) { + const vNode = domNode[INTERNAL_VNODE]; + if (vNode) { + return vNode; + } + domNode = domNode.parentNode; } - // 下面处理的是为被框架标记过的 DOM 节点,向上找其父节点是否被框架标记过 - let parentDom = dom.parentNode; - let nearVNode = null; - while (parentDom) { - vNode = parentDom[INTERNAL_VNODE]; - if (vNode) { - nearVNode = vNode; - break; - } - parentDom = parentDom.parentNode; - } - return nearVNode; + return null; } // 获取 vNode 上的属性相关信息 diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts index 49429fbe..b93e1581 100644 --- a/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts +++ b/libs/horizon/src/dom/DOMPropertiesHandler/StyleHandler.ts @@ -1,12 +1,12 @@ -function isNeedUnitCSS(propName: string) { - return !(noUnitCSS.includes(propName) - || propName.startsWith('borderImage') - || propName.startsWith('flex') - || propName.startsWith('gridRow') - || propName.startsWith('gridColumn') - || propName.startsWith('stroke') - || propName.startsWith('box') - || propName.endsWith('Opacity')); +function isNeedUnitCSS(styleName: string) { + return !(noUnitCSS.includes(styleName) + || styleName.startsWith('borderImage') + || styleName.startsWith('flex') + || styleName.startsWith('gridRow') + || styleName.startsWith('gridColumn') + || styleName.startsWith('stroke') + || styleName.startsWith('box') + || styleName.endsWith('Opacity')); } /** @@ -38,9 +38,7 @@ export function setStyles(dom, styles) { Object.keys(styles).forEach((name) => { const styleVal = styles[name]; - const validStyleValue = adjustStyleValue(name, styleVal); - - style[name] = validStyleValue; + style[name] = adjustStyleValue(name, styleVal); }); } diff --git a/libs/horizon/src/dom/validators/ValidateProps.ts b/libs/horizon/src/dom/validators/ValidateProps.ts index 5616f342..9dcf4a23 100644 --- a/libs/horizon/src/dom/validators/ValidateProps.ts +++ b/libs/horizon/src/dom/validators/ValidateProps.ts @@ -6,7 +6,7 @@ const INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/; // 是内置元素 -export function isNativeElement(tagName: string, props: Object) { +export function isNativeElement(tagName: string, props: Record) { return !tagName.includes('-') && props.is === undefined; } diff --git a/libs/horizon/src/dom/valueHandler/index.ts b/libs/horizon/src/dom/valueHandler/index.ts index 88a35e5c..2e5b6da7 100644 --- a/libs/horizon/src/dom/valueHandler/index.ts +++ b/libs/horizon/src/dom/valueHandler/index.ts @@ -21,7 +21,7 @@ import { getTextareaPropsWithoutValue, updateTextareaValue, } from './TextareaValueHandler'; -import {getDomTag} from "../utils/Common"; +import {getDomTag} from '../utils/Common'; // 获取元素除了被代理的值以外的属性 function getPropsWithoutValue(type: string, dom: HorizonDom, properties: IProperty) { diff --git a/libs/horizon/src/event/HorizonEventMain.ts b/libs/horizon/src/event/HorizonEventMain.ts index 08e9f5d0..f0480724 100644 --- a/libs/horizon/src/event/HorizonEventMain.ts +++ b/libs/horizon/src/event/HorizonEventMain.ts @@ -16,8 +16,11 @@ import { decorateNativeEvent } from './customEvents/EventFactory'; import { getListenersFromTree } from './ListenerGetter'; import { shouldUpdateValue, updateControlledValue } from './ControlledValueUpdater'; import { asyncUpdates, runDiscreteUpdates } from '../renderer/Renderer'; -import { getExactNode } from '../renderer/vnode/VNodeUtils'; -import {ListenerUnitList} from './Types'; +import { findRoot } from '../renderer/vnode/VNodeUtils'; +import { ListenerUnitList } from './Types'; + +// web规范,鼠标右键key值 +const RIGHT_MOUSE_BUTTON = 2; // 获取事件触发的普通事件监听方法队列 function getCommonListeners( @@ -29,13 +32,13 @@ function getCommonListeners( ): ListenerUnitList { const name = CommonEventToHorizonMap[nativeEvtName]; const horizonEvtName = !name ? '' : `on${name[0].toUpperCase()}${name.slice(1)}`; // 例:dragEnd -> onDragEnd - + if (!horizonEvtName) { return []; } // 鼠标点击右键 - if (nativeEvent instanceof MouseEvent && nativeEvtName === 'click' && nativeEvent.button === 2) { + if (nativeEvent instanceof MouseEvent && nativeEvtName === 'click' && nativeEvent.button === RIGHT_MOUSE_BUTTON) { return []; } @@ -76,7 +79,7 @@ function getProcessListeners( vNode: VNode | null, nativeEvent: AnyNativeEvent, target, - isCapture: boolean + isCapture: boolean, ): ListenerUnitList { // 触发普通委托事件 let listenerList: ListenerUnitList = getCommonListeners( @@ -136,11 +139,11 @@ export function handleEventMain( isCapture: boolean, nativeEvent: AnyNativeEvent, vNode: null | VNode, - targetContainer: EventTarget, + targetDom: EventTarget, ): void { let startVNode = vNode; if (startVNode !== null) { - startVNode = getExactNode(startVNode, targetContainer); + startVNode = findRoot(startVNode, targetDom); if (!startVNode) { return; } diff --git a/libs/horizon/src/renderer/vnode/VNodeUtils.ts b/libs/horizon/src/renderer/vnode/VNodeUtils.ts index ef60b566..a586b922 100644 --- a/libs/horizon/src/renderer/vnode/VNodeUtils.ts +++ b/libs/horizon/src/renderer/vnode/VNodeUtils.ts @@ -31,7 +31,6 @@ export function travelVNodeTree( finishVNode: VNode, // 结束遍历节点,有时候和beginVNode不相同 handleWhenToParent: Function | null ): VNode | null { - const filter = childFilter === null; let node = beginVNode; while (true) { @@ -43,7 +42,7 @@ export function travelVNodeTree( // 找子节点 const childVNode = node.child; - if (childVNode !== null && (filter || !childFilter(node))) { + if (childVNode !== null && (childFilter === null || !childFilter(node))) { childVNode.parent = node; node = childVNode; continue; @@ -194,20 +193,6 @@ export function getSiblingDom(vNode: VNode): Element | null { } } -function isSameContainer( - container: Element, - targetContainer: EventTarget, -): boolean { - if (container === targetContainer) { - return true; - } - // 注释类型的节点 - if (isComment(container) && container.parentNode === targetContainer) { - return true; - } - return false; -} - function isPortalRoot(vNode, targetContainer) { if (vNode.tag === DomPortal) { let topVNode = vNode.parent; @@ -216,7 +201,7 @@ function isPortalRoot(vNode, targetContainer) { if (grandTag === TreeRoot || grandTag === DomPortal) { const topContainer = topVNode.realNode; // 如果topContainer是targetContainer,不需要在这里处理 - if (isSameContainer(topContainer, targetContainer)) { + if (topContainer === targetContainer) { return true; } } @@ -228,28 +213,28 @@ function isPortalRoot(vNode, targetContainer) { } // 获取根vNode节点 -export function getExactNode(targetVNode, targetContainer) { +export function findRoot(targetVNode, targetDom) { // 确认vNode节点是否准确,portal场景下可能祖先节点不准确 let vNode = targetVNode; while (vNode !== null) { if (vNode.tag === TreeRoot || vNode.tag === DomPortal) { - let container = vNode.realNode; - if (isSameContainer(container, targetContainer)) { + let dom = vNode.realNode; + if (dom === targetDom) { break; } - if (isPortalRoot(vNode, targetContainer)) { + if (isPortalRoot(vNode, targetDom)) { return null; } - while (container !== null) { - const parentNode = getNearestVNode(container); + while (dom !== null) { + const parentNode = getNearestVNode(dom); if (parentNode === null) { return null; } if (parentNode.tag === DomComponent || parentNode.tag === DomText) { - return getExactNode(parentNode, targetContainer); + return findRoot(parentNode, targetDom); } - container = container.parentNode; + dom = dom.parentNode; } } vNode = vNode.parent; diff --git a/scripts/__tests__/DomTest/Attribute.test.js b/scripts/__tests__/DomTest/Attribute.test.js index 49030e2f..32fe44a5 100755 --- a/scripts/__tests__/DomTest/Attribute.test.js +++ b/scripts/__tests__/DomTest/Attribute.test.js @@ -61,4 +61,10 @@ describe('Dom Attribute', () => { container.querySelector('div').setAttribute('data-first-name', 'Tom'); expect(container.querySelector('div').dataset.firstName).toBe('Tom'); }); -}); \ No newline at end of file + + it('style 自动加px', () => { + const div = Horizon.render(
, container); + expect(window.getComputedStyle(div).getPropertyValue('width')).toBe('10px'); + expect(window.getComputedStyle(div).getPropertyValue('height')).toBe('20px'); + }); +}); diff --git a/scripts/__tests__/DomTest/DomInput.test.js b/scripts/__tests__/DomTest/DomInput.test.js index d5649ff3..7d43b251 100755 --- a/scripts/__tests__/DomTest/DomInput.test.js +++ b/scripts/__tests__/DomTest/DomInput.test.js @@ -172,6 +172,11 @@ describe('Dom Input', () => { expect(realNode.getAttribute('value')).toBe('default'); }); + it('value为0、defaultValue为1,input 的value应该为0', () => { + const input = Horizon.render(, container); + expect(input.getAttribute('value')).toBe('0'); + }); + it('name属性', () => { let realNode = Horizon.render(, container); expect(realNode.name).toBe('name'); @@ -426,4 +431,4 @@ describe('Dom Input', () => { expect(container.querySelector('input').hasAttribute('value')).toBe(false); }); }); -}); \ No newline at end of file +}); From c34df0d5f47a45f1e55e21e31974b6fb183d08f7 Mon Sep 17 00:00:00 2001 From: * <8> Date: Fri, 13 May 2022 11:00:00 +0800 Subject: [PATCH 2/4] Match-id-4c5a3a22ce4ffd2686921714db97c18b77f16288 --- libs/horizon/src/dom/DOMExternal.ts | 2 +- .../DOMPropertiesHandler.ts | 9 ++++- .../dom/valueHandler/SelectValueHandler.ts | 2 +- libs/horizon/src/event/EventBinding.ts | 37 +++++++++++++------ libs/horizon/src/event/HorizonEventMain.ts | 11 +----- libs/horizon/src/event/const.ts | 3 +- .../simulatedEvtHandler/ChangeEventHandler.ts | 5 +++ libs/horizon/src/event/utils.ts | 6 +-- libs/horizon/src/renderer/TreeBuilder.ts | 8 +++- libs/horizon/src/renderer/submit/Submit.ts | 8 ++-- libs/horizon/src/renderer/vnode/VNode.ts | 4 ++ 11 files changed, 59 insertions(+), 36 deletions(-) diff --git a/libs/horizon/src/dom/DOMExternal.ts b/libs/horizon/src/dom/DOMExternal.ts index 077d0801..02f11e53 100644 --- a/libs/horizon/src/dom/DOMExternal.ts +++ b/libs/horizon/src/dom/DOMExternal.ts @@ -23,7 +23,7 @@ function createRoot(children: any, container: Container, callback?: Callback) { container._treeRoot = treeRoot; // 根节点挂接全量事件 - listenDelegatedEvents(container as Element); + // listenDelegatedEvents(container as Element); // 执行回调 if (typeof callback === 'function') { diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts index c7f76f2f..1ac626a0 100644 --- a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts +++ b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts @@ -4,9 +4,11 @@ import { import { updateCommonProp } from './UpdateCommonProp'; import { setStyles } from './StyleHandler'; import { - listenNonDelegatedEvent + lazyDelegateOnRoot, + listenNonDelegatedEvent, } from '../../event/EventBinding'; import { isEventProp } from '../validators/ValidateProps'; +import { getCurrentRoot } from '../../renderer/TreeBuilder'; // 初始化DOM属性和更新 DOM 属性 export function setDomProps( @@ -27,8 +29,12 @@ export function setDomProps( setStyles(dom, propVal); } else if (isEventProp(propName)) { // 事件监听属性处理 + // TODO + const currentRoot = getCurrentRoot(); if (!allDelegatedHorizonEvents.has(propName)) { listenNonDelegatedEvent(propName, dom, propVal); + } else if (currentRoot && !currentRoot.delegatedEvents.has(propName)) { + lazyDelegateOnRoot(currentRoot, propName); } } else if (propName === 'children') { // 只处理纯文本子节点,其他children在VNode树中处理 const type = typeof propVal; @@ -147,6 +153,7 @@ export function compareProps( if (!allDelegatedHorizonEvents.has(propName)) { toUpdateProps[propName] = newPropValue; } + // TODO } else { toUpdateProps[propName] = newPropValue; } diff --git a/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts b/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts index 75812620..d7c5852f 100644 --- a/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts +++ b/libs/horizon/src/dom/valueHandler/SelectValueHandler.ts @@ -43,7 +43,7 @@ export function getSelectPropsWithoutValue(dom: HorizonSelect, properties: Objec return { ...properties, value: undefined, - } + }; } export function updateSelectValue(dom: HorizonSelect, properties: IProperty, isInit: boolean = false) { diff --git a/libs/horizon/src/event/EventBinding.ts b/libs/horizon/src/event/EventBinding.ts index 54d3fdb4..66bf94a7 100644 --- a/libs/horizon/src/event/EventBinding.ts +++ b/libs/horizon/src/event/EventBinding.ts @@ -1,7 +1,7 @@ /** * 事件绑定实现,分为绑定委托事件和非委托事件 */ -import {allDelegatedNativeEvents} from './EventCollection'; +import { allDelegatedHorizonEvents, allDelegatedNativeEvents } from './EventCollection'; import {isDocument} from '../dom/utils/Common'; import { getNearestVNode, @@ -12,6 +12,7 @@ import {isMounted} from '../renderer/vnode/VNodeUtils'; import {SuspenseComponent} from '../renderer/vnode/VNodeTags'; import {handleEventMain} from './HorizonEventMain'; import {decorateNativeEvent} from './customEvents/EventFactory'; +import { VNode } from '../renderer/vnode/VNode'; const listeningMarker = '_horizonListening' + Math.random().toString(36).slice(4); @@ -26,18 +27,20 @@ function triggerDelegatedEvent( runDiscreteUpdates(); const nativeEventTarget = nativeEvent.target || nativeEvent.srcElement; - let targetVNode = getNearestVNode(nativeEventTarget); + const targetVNode = getNearestVNode(nativeEventTarget); - if (targetVNode !== null) { - if (isMounted(targetVNode)) { - if (targetVNode.tag === SuspenseComponent) { - targetVNode = null; - } - } else { - // vNode已销毁 - targetVNode = null; - } - } + // if (targetVNode !== null) { + // if (isMounted(targetVNode)) { + // if (targetVNode.tag === SuspenseComponent) { + // debugger + // targetVNode = null; + // } + // } else { + // debugger + // // vNode已销毁 + // targetVNode = null; + // } + // } handleEventMain(nativeEvtName, isCapture, nativeEvent, targetVNode, targetDom); } @@ -73,6 +76,16 @@ export function listenDelegatedEvents(dom: Element) { }); } +// 事件懒委托,当用户定义事件后,再进行委托到根节点 +export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) { + currentRoot.delegatedEvents.add(eventName); + + const isCapture = isCaptureEvent(eventName); + const nativeEvents = allDelegatedHorizonEvents.get(eventName); + nativeEvents.forEach(nativeEvents => { + listenToNativeEvent(nativeEvents, currentRoot.realNode, isCapture); + }); +} // 通过horizon事件名获取到native事件名 function getNativeEvtName(horizonEventName, capture) { let nativeName; diff --git a/libs/horizon/src/event/HorizonEventMain.ts b/libs/horizon/src/event/HorizonEventMain.ts index f0480724..35db487b 100644 --- a/libs/horizon/src/event/HorizonEventMain.ts +++ b/libs/horizon/src/event/HorizonEventMain.ts @@ -8,7 +8,6 @@ import { EVENT_TYPE_CAPTURE, } from './const'; import { getListeners as getChangeListeners } from './simulatedEvtHandler/ChangeEventHandler'; -import { getListeners as getSelectionListeners } from './simulatedEvtHandler/SelectionEventHandler'; import { setPropertyWritable, } from './utils'; @@ -81,6 +80,7 @@ function getProcessListeners( target, isCapture: boolean, ): ListenerUnitList { + // TODO 重复从树中获取监听器 // 触发普通委托事件 let listenerList: ListenerUnitList = getCommonListeners( nativeEvtName, @@ -100,15 +100,6 @@ function getProcessListeners( target, )); } - - if (horizonEventToNativeMap.get('onSelect').includes(nativeEvtName)) { - listenerList = listenerList.concat(getSelectionListeners( - nativeEvtName, - nativeEvent, - vNode, - target, - )); - } } return listenerList; } diff --git a/libs/horizon/src/event/const.ts b/libs/horizon/src/event/const.ts index 449663e9..c0e185a4 100644 --- a/libs/horizon/src/event/const.ts +++ b/libs/horizon/src/event/const.ts @@ -28,8 +28,7 @@ export const horizonEventToNativeMap = new Map([ ['onCompositionStart', ['compositionstart']], ['onCompositionUpdate', ['compositionupdate']], ['onChange', ['change', 'click', 'focusout', 'input']], - ['onSelect', ['focusout', 'contextmenu', 'dragend', 'focusin', - 'keydown', 'keyup', 'mousedown', 'mouseup', 'selectionchange']], + ['onSelect', ['select']], ['onAnimationEnd', ['animationend']], ['onAnimationIteration', ['animationiteration']], diff --git a/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts b/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts index c0ad947d..f1b781af 100644 --- a/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts +++ b/libs/horizon/src/event/simulatedEvtHandler/ChangeEventHandler.ts @@ -12,6 +12,11 @@ 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); diff --git a/libs/horizon/src/event/utils.ts b/libs/horizon/src/event/utils.ts index 09d1b240..37ec6218 100644 --- a/libs/horizon/src/event/utils.ts +++ b/libs/horizon/src/event/utils.ts @@ -1,9 +1,7 @@ export function isInputElement(dom?: HTMLElement): boolean { - if (dom instanceof HTMLInputElement || dom instanceof HTMLTextAreaElement) { - return true; - } - return false; + return dom instanceof HTMLInputElement || dom instanceof HTMLTextAreaElement; + } export function setPropertyWritable(obj, propName) { diff --git a/libs/horizon/src/renderer/TreeBuilder.ts b/libs/horizon/src/renderer/TreeBuilder.ts index 12626215..32bc6c14 100644 --- a/libs/horizon/src/renderer/TreeBuilder.ts +++ b/libs/horizon/src/renderer/TreeBuilder.ts @@ -43,6 +43,11 @@ let unrecoverableErrorDuringBuild: any = null; // 当前运行的vNode节点 let processing: VNode | null = null; +let currentRoot: VNode | null = null; +export function getCurrentRoot() { + return currentRoot; +} + export function setProcessing(vNode: VNode | null) { processing = vNode; } @@ -267,7 +272,7 @@ function buildVNodeTree(treeRoot: VNode) { // 总体任务入口 function renderFromRoot(treeRoot) { runAsyncEffects(); - + currentRoot = treeRoot; // 1. 构建vNode树 buildVNodeTree(treeRoot); @@ -278,6 +283,7 @@ function renderFromRoot(treeRoot) { // 2. 提交变更 submitToRender(treeRoot); + currentRoot = null; if (window.__HORIZON_DEV_HOOK__) { const hook = window.__HORIZON_DEV_HOOK__; diff --git a/libs/horizon/src/renderer/submit/Submit.ts b/libs/horizon/src/renderer/submit/Submit.ts index e80f911a..d19dfea2 100644 --- a/libs/horizon/src/renderer/submit/Submit.ts +++ b/libs/horizon/src/renderer/submit/Submit.ts @@ -114,21 +114,21 @@ function submit(dirtyNodes: Array) { if ((node.flags & ResetText) === ResetText) { submitResetTextContent(node); } - + if ((node.flags & Ref) === Ref) { if (!node.isCreated) { // 需要执行 detachRef(node, true); } } - + isAdd = (node.flags & Addition) === Addition; isUpdate = (node.flags & Update) === Update; if (isAdd && isUpdate) { // Addition submitAddition(node); FlagUtils.removeFlag(node, Addition); - + // Update submitUpdate(node); } else { @@ -161,7 +161,7 @@ function afterSubmit(dirtyNodes: Array) { if ((node.flags & Update) === Update || (node.flags & Callback) === Callback) { callAfterSubmitLifeCycles(node); } - + if ((node.flags & Ref) === Ref) { attachRef(node); } diff --git a/libs/horizon/src/renderer/vnode/VNode.ts b/libs/horizon/src/renderer/vnode/VNode.ts index d3e6c23a..d67f0e73 100644 --- a/libs/horizon/src/renderer/vnode/VNode.ts +++ b/libs/horizon/src/renderer/vnode/VNode.ts @@ -74,7 +74,10 @@ export class VNode { suspenseState: SuspenseState; path = ''; // 保存从根到本节点的路径 + + // 根节点数据 toUpdateNodes: Set | null; // 保存要更新的节点 + delegatedEvents: Set belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode,处理ref的时候使用 @@ -94,6 +97,7 @@ export class VNode { this.realNode = realNode; this.task = null; this.toUpdateNodes = new Set(); + this.delegatedEvents = new Set(); this.updates = null; this.stateCallbacks = null; this.state = null; From 9802399b1f72b910730df3b972e191be3dfc527f Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 21 Jun 2022 17:50:27 +0800 Subject: [PATCH 3/4] Match-id-ff887de05a36a9d53176b27c20249d9d7588d755 --- libs/horizon/src/dom/DOMOperator.ts | 2 +- .../DOMPropertiesHandler.ts | 34 ++--- libs/horizon/src/dom/utils/Common.ts | 4 + .../src/dom/valueHandler/InputValueHandler.ts | 55 ++++---- .../dom/valueHandler/ValueChangeHandler.ts | 2 +- libs/horizon/src/dom/valueHandler/index.ts | 20 --- .../src/event/ControlledValueUpdater.ts | 37 ------ libs/horizon/src/event/HorizonEventMain.ts | 34 +++-- libs/horizon/src/event/const.ts | 1 + .../simulatedEvtHandler/ChangeEventHandler.ts | 11 +- .../SelectionEventHandler.ts | 112 ---------------- package.json | 1 + scripts/__tests__/EventTest/EventMain.test.js | 120 +++++++++++++++--- scripts/template.ejs | 4 + 14 files changed, 171 insertions(+), 266 deletions(-) delete mode 100644 libs/horizon/src/event/ControlledValueUpdater.ts delete mode 100644 libs/horizon/src/event/simulatedEvtHandler/SelectionEventHandler.ts 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')}>