From 93f529b55e565f61702c98ab4ee5283eccb14a99 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 31 Aug 2022 17:06:27 +0800 Subject: [PATCH 01/24] Match-id-1ba61a1afe0d2bd9e6e730e66daf9f39970e4499 --- libs/horizon/index.ts | 3 + libs/horizon/src/horizonx/proxy/Observer.ts | 2 + .../proxy/handlers/ArrayProxyHandler.ts | 20 ++ .../proxy/handlers/CollectionProxyHandler.ts | 18 ++ .../proxy/handlers/ObjectProxyHandler.ts | 17 ++ libs/horizon/src/horizonx/proxy/watch.ts | 8 + .../src/horizonx/store/StoreHandler.ts | 10 +- .../src/renderer/render/BaseComponent.ts | 3 + .../StoreFunctionality/watch.test.tsx | 132 ++++++++++++ .../edgeCases/multipleStores.test.tsx | 199 ++++++++++++++++++ 10 files changed, 406 insertions(+), 6 deletions(-) create mode 100644 libs/horizon/src/horizonx/proxy/watch.ts create mode 100644 scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx create mode 100644 scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx diff --git a/libs/horizon/index.ts b/libs/horizon/index.ts index 9e29e71d..f8035375 100644 --- a/libs/horizon/index.ts +++ b/libs/horizon/index.ts @@ -33,6 +33,7 @@ import { runAsyncEffects } from './src/renderer/submit/HookEffectHandler'; import { createStore, useStore, clearStore } from './src/horizonx/store/StoreHandler'; import * as reduxAdapter from './src/horizonx/adapters/redux'; +import { watch } from './src/horizonx/proxy/watch'; // act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。 const act = fun => { @@ -85,6 +86,7 @@ const Horizon = { useStore, clearStore, reduxAdapter, + watch }; export const version = __VERSION__; @@ -125,6 +127,7 @@ export { useStore, clearStore, reduxAdapter, + watch }; export default Horizon; diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index d93c7a05..b00e942e 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -33,6 +33,8 @@ export class Observer implements IObserver { listeners:(()=>void)[] = []; + watchers={} as {[key:string]:((key:string, oldValue:any, newValue:any)=>void)[]} + useProp(key: string | symbol): void { const processingVNode = getProcessingVNode(); if (processingVNode === null || !processingVNode.observers) { diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index a6812bde..dd3fdb0b 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -12,6 +12,20 @@ export function createArrayProxy(rawObj: any[]): any[] { } function get(rawObj: any[], key: string, receiver: any) { + if (key === 'watch'){ + const observer = getObserver(rawObj); + + return (prop:any, handler:(key:string, oldValue:any, newValue:any)=>void)=>{ + if(!observer.watchers[prop]){ + observer.watchers[prop]=[] as ((key:string, oldValue:any, newValue:any)=>void)[]; + } + observer.watchers[prop].push(handler); + return ()=>{ + observer.watchers[prop]=observer.watchers[prop].filter(cb=>cb!==handler); + } + } + } + if (isValidIntegerKey(key) || key === 'length') { return objectGet(rawObj, key, receiver); } @@ -29,6 +43,12 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { const observer = getObserver(rawObj); if (!isSame(newValue, oldValue)) { + if(observer.watchers?.[key]){ + observer.watchers[key].forEach(cb => { + cb(key, oldValue, newValue); + }); + } + observer.setProp(key); } diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts index bd044b44..15aaa682 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -34,6 +34,18 @@ function get(rawObj: { size: number }, key: any, receiver: any): any { } else if (Object.prototype.hasOwnProperty.call(handler, key)) { const value = Reflect.get(handler, key, receiver); return value.bind(null, rawObj); + } else if (key === 'watch'){ + const observer = getObserver(rawObj); + + return (prop:any, handler:(key:string, oldValue:any, newValue:any)=>void)=>{ + if(!observer.watchers[prop]){ + observer.watchers[prop]=[] as ((key:string, oldValue:any, newValue:any)=>void)[]; + } + observer.watchers[prop].push(handler); + return ()=>{ + observer.watchers[prop]=observer.watchers[prop].filter(cb=>cb!==handler); + } + } } return Reflect.get(rawObj, key, receiver); @@ -67,6 +79,12 @@ function set( } if (valChange) { + if(observer.watchers?.[key]){ + observer.watchers[key].forEach(cb => { + cb(key, oldValue, newValue); + }); + } + observer.setProp(key); } diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index 4dc27a0b..29ab2658 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -19,6 +19,18 @@ export function get(rawObj: object, key: string | symbol, receiver: any, singleL const observer = getObserver(rawObj); + if (key === 'watch'){ + return (prop, handler:(key:string, oldValue:any, newValue:any)=>void)=>{ + if(!observer.watchers[prop]){ + observer.watchers[prop]=[] as ((key:string, oldValue:any, newValue:any)=>void)[]; + } + observer.watchers[prop].push(handler); + return ()=>{ + observer.watchers[prop]=observer.watchers[prop].filter(cb=>cb!==handler); + } + } + } + if (key === 'addListener') { return observer.addListener.bind(observer); } @@ -54,6 +66,11 @@ export function set(rawObj: object, key: string, value: any, receiver: any): boo const ret = Reflect.set(rawObj, key, newValue, receiver); if (!isSame(newValue, oldValue)) { + if(observer.watchers?.[key]){ + observer.watchers[key].forEach(cb => { + cb(key, oldValue, newValue); + }); + } observer.setProp(key); } diff --git a/libs/horizon/src/horizonx/proxy/watch.ts b/libs/horizon/src/horizonx/proxy/watch.ts new file mode 100644 index 00000000..79328142 --- /dev/null +++ b/libs/horizon/src/horizonx/proxy/watch.ts @@ -0,0 +1,8 @@ +export function watch(stateVariable:any,listener:(state:any)=>void){ + listener = listener.bind(null,stateVariable); + stateVariable.addListener(listener); + + return ()=>{ + stateVariable.removeListener(listener); + } +} \ No newline at end of file diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index bdbf8f77..00d4695e 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -204,7 +204,8 @@ export function createStore, C extend return createStoreHook(handler); } -function clearVNodeObservers(vNode) { +export function clearVNodeObservers(vNode) { + if(!vNode.observers) return; vNode.observers.forEach(observer => { observer.clearByVNode(vNode); }); @@ -219,11 +220,8 @@ function hookStore() { if (!processingVNode) { return; } - - if (processingVNode.observers) { - // 清除上一次缓存的Observer依赖 - clearVNodeObservers(processingVNode); - } else { + + if (!processingVNode.observers) { processingVNode.observers = new Set(); } diff --git a/libs/horizon/src/renderer/render/BaseComponent.ts b/libs/horizon/src/renderer/render/BaseComponent.ts index 1ea097d3..4192e100 100644 --- a/libs/horizon/src/renderer/render/BaseComponent.ts +++ b/libs/horizon/src/renderer/render/BaseComponent.ts @@ -12,6 +12,7 @@ import { FlagUtils } from '../vnode/VNodeFlags'; import {onlyUpdateChildVNodes} from '../vnode/VNodeCreator'; import componentRenders from './index'; import {setProcessingVNode} from '../GlobalVar'; +import { clearVNodeObservers } from '../../horizonx/store/StoreHandler'; // 复用vNode时,也需对stack进行处理 function handlerContext(processing: VNode) { @@ -55,6 +56,8 @@ export function captureVNode(processing: VNode): VNode | null { processing.shouldUpdate = false; setProcessingVNode(processing); + + if(processing.observers) clearVNodeObservers(processing); const child = component.captureRender(processing, shouldUpdate); setProcessingVNode(null); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx new file mode 100644 index 00000000..806c6029 --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx @@ -0,0 +1,132 @@ +import { createStore } from "@cloudsop/horizon/src/horizonx/store/StoreHandler"; +import { watch } from "@cloudsop/horizon/src/horizonx/proxy/watch"; + +describe("watch",()=>{ + it('shouhld watch promitive state variable', async()=>{ + const useStore = createStore({ + state:{ + variable:'x' + }, + actions:{ + change:(state)=>state.variable = "a" + } + }); + + const store = useStore(); + let counter = 0; + + watch(store.$s,(state)=>{ + counter++; + expect(state.variable).toBe('a'); + }) + + store.change(); + + expect(counter).toBe(1); + }); + it('shouhld watch object variable', async()=>{ + const useStore = createStore({ + state:{ + variable:'x' + }, + actions:{ + change:(state)=>state.variable = "a" + } + }); + + const store = useStore(); + let counter = 0; + + store.$s.watch('variable',()=>{ + counter++; + }) + + store.change(); + + expect(counter).toBe(1); + }); + + it('shouhld watch array item', async()=>{ + const useStore = createStore({ + state:{ + arr:['x'] + }, + actions:{ + change:(state)=>state.arr[0]='a' + } + }); + + const store = useStore(); + let counter = 0; + + store.arr.watch('0',()=>{ + counter++; + }) + + store.change(); + + expect(counter).toBe(1); + }); + + it('shouhld watch collection item', async()=>{ + const useStore = createStore({ + state:{ + collection:new Map([ + ['a', 'a'], + ]) + }, + actions:{ + change:(state)=>state.collection.set('a','x') + } + }); + + const store = useStore(); + let counter = 0; + + store.collection.watch('a',()=>{ + counter++; + }) + + store.change(); + + expect(counter).toBe(1); + }); + + it('should watch multiple variables independedntly', async()=>{ + const useStore = createStore({ + state:{ + bool1:true, + bool2:false + }, + actions:{ + toggle1:state=>state.bool1=!state.bool1, + toggle2:state=>state.bool2=!state.bool2 + } + }); + + let counter1=0; + let counterAll=0; + const store = useStore(); + + watch(store.$s,()=>{ + counterAll++; + }) + + store.$s.watch('bool1',()=>{ + counter1++; + }); + + store.toggle1(); + store.toggle1(); + + store.toggle2(); + + store.toggle1(); + + store.toggle2(); + store.toggle2(); + + expect(counter1).toBe(3); + expect(counterAll).toBe(6); + }) +}) \ No newline at end of file diff --git a/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx b/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx new file mode 100644 index 00000000..63fb74e5 --- /dev/null +++ b/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx @@ -0,0 +1,199 @@ +//@ts-ignore +import Horizon, { createStore } from '@cloudsop/horizon/index.ts'; +import { triggerClickEvent } from '../../jest/commonComponents'; +import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; + +const { unmountComponentAtNode } = Horizon; + +const useStore1 = createStore({ + state:{ counter:1 }, + actions:{ + add:(state)=>state.counter++, + reset: (state)=>state.counter=1 + } +}) + +const useStore2 = createStore({ + state:{ counter2:1 }, + actions:{ + add2:(state)=>state.counter2++, + reset: (state)=>state.counter2=1 + } +}) + +describe('Using multiple stores', () => { + let container: HTMLElement | null = null; + + const BUTTON_ID = 'btn'; + const BUTTON_ID2 = 'btn2'; + const RESULT_ID = 'result'; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + useStore1().reset(); + useStore2().reset(); + }); + + afterEach(() => { + unmountComponentAtNode(container); + container?.remove(); + container = null; + }); + + it('Should use multiple stores in class component', () => { + class App extends Horizon.Component{ + render(){ + const {counter,add} = useStore1(); + const {counter2, add2} = useStore2(); + + return ( +
+ + +

{counter} {counter2}

+
+ ) + } + } + + Horizon.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID2); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); + + }); + + it('Should use use stores in cycles and multiple methods', () => { + interface App { + store:any, + store2:any + } + class App extends Horizon.Component{ + constructor(){ + super(); + this.store = useStore1(); + this.store2 = useStore2() + } + + render(){ + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + + for(let i=0; i<100; i++){ + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + } + + return ( +
+ + +

{counter} {counter2}

+
+ ) + } + } + + Horizon.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID2); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); + + }); + + it('Should use multiple stores in function component', () => { + function App() { + const {counter,add} = useStore1(); + const store2 = useStore2(); + const {counter2, add2} = store2; + + return ( +
+ + +

{counter} {counter2}

+
+ ); + } + + Horizon.render(, container); + + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); + + Horizon.act(() => { + triggerClickEvent(container, BUTTON_ID2); + + }); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); + }); +}); \ No newline at end of file From ee703a78a9b7e50a584329d7130b7b6cdd08a783 Mon Sep 17 00:00:00 2001 From: * <8> Date: Sun, 4 Sep 2022 19:26:32 +0800 Subject: [PATCH 02/24] Match-id-5650edda61e31f38fc56e1f5acc8c472870909f2 --- libs/horizon/src/dom/DOMExternal.ts | 43 ++++++++++++++---------- libs/horizon/src/dom/DOMOperator.ts | 2 +- libs/horizon/src/event/EventBinding.ts | 10 +++--- libs/horizon/src/renderer/vnode/VNode.ts | 4 +-- 4 files changed, 34 insertions(+), 25 deletions(-) diff --git a/libs/horizon/src/dom/DOMExternal.ts b/libs/horizon/src/dom/DOMExternal.ts index 5050e3f5..6b82889f 100644 --- a/libs/horizon/src/dom/DOMExternal.ts +++ b/libs/horizon/src/dom/DOMExternal.ts @@ -1,14 +1,9 @@ -import { - asyncUpdates, getFirstCustomDom, - syncUpdates, startUpdate, - createTreeRootVNode, -} from '../renderer/Renderer'; -import {createPortal} from '../renderer/components/CreatePortal'; -import type {Container} from './DOMOperator'; -import {isElement} from './utils/Common'; -import {listenDelegatedEvents} from '../event/EventBinding'; -import {findDOMByClassInst} from '../renderer/vnode/VNodeUtils'; -import {Callback} from '../renderer/UpdateHandler'; +import { asyncUpdates, getFirstCustomDom, syncUpdates, startUpdate, createTreeRootVNode } from '../renderer/Renderer'; +import { createPortal } from '../renderer/components/CreatePortal'; +import type { Container } from './DOMOperator'; +import { isElement } from './utils/Common'; +import { findDOMByClassInst } from '../renderer/vnode/VNodeUtils'; +import { Callback } from '../renderer/UpdateHandler'; function createRoot(children: any, container: Container, callback?: Callback) { // 清空容器 @@ -39,16 +34,13 @@ function createRoot(children: any, container: Container, callback?: Callback) { return treeRoot; } -function executeRender( - children: any, - container: Container, - callback?: Callback, -) { +function executeRender(children: any, container: Container, callback?: Callback) { let treeRoot = container._treeRoot; if (!treeRoot) { treeRoot = createRoot(children, container, callback); - } else { // container被render过 + } else { + // container被render过 if (typeof callback === 'function') { const cb = callback; callback = function () { @@ -77,11 +69,26 @@ function findDOMNode(domOrEle?: Element): null | Element | Text { return findDOMByClassInst(domOrEle); } +// 情况根节点监听器 +function removeRootEventLister(container: Container) { + const root = container._treeRoot; + if (root) { + Object.keys(root.delegatedNativeEvents).forEach(event => { + const listener = root.delegatedNativeEvents[event]; + + if (listener) { + container.removeEventListener(event, listener); + } + }); + } +} + // 卸载入口 -function destroy(container: Container) { +function destroy(container: Container): boolean { if (container._treeRoot) { syncUpdates(() => { executeRender(null, container, () => { + removeRootEventLister(container); container._treeRoot = null; }); }); diff --git a/libs/horizon/src/dom/DOMOperator.ts b/libs/horizon/src/dom/DOMOperator.ts index 07700dd4..85b35da4 100644 --- a/libs/horizon/src/dom/DOMOperator.ts +++ b/libs/horizon/src/dom/DOMOperator.ts @@ -35,7 +35,7 @@ export type Props = Record & { style?: { display?: string }; }; -export type Container = (Element & { _treeRoot?: VNode }) | (Document & { _treeRoot?: VNode }); +export type Container = (Element & { _treeRoot?: VNode | null }) | (Document & { _treeRoot?: VNode | null }); let selectionInfo: null | SelectionData = null; diff --git a/libs/horizon/src/event/EventBinding.ts b/libs/horizon/src/event/EventBinding.ts index 5a38e45b..40e7625a 100644 --- a/libs/horizon/src/event/EventBinding.ts +++ b/libs/horizon/src/event/EventBinding.ts @@ -35,7 +35,7 @@ function triggerDelegatedEvent( } // 监听委托事件 -function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, isCapture: boolean): void { +function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, isCapture: boolean) { let dom: Element | Document = delegatedElement; // document层次可能触发selectionchange事件,为了捕获这类事件,selectionchange事件绑定在document节点上 if (nativeEvtName === 'selectionchange' && !isDocument(delegatedElement)) { @@ -44,6 +44,8 @@ function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, i const listener = triggerDelegatedEvent.bind(null, nativeEvtName, isCapture, dom); dom.addEventListener(nativeEvtName, listener, isCapture); + + return listener; } // 监听所有委托事件 @@ -71,9 +73,9 @@ export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) { nativeEvents.forEach(nativeEvent => { const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent; - if (!currentRoot.delegatedNativeEvents.has(nativeFullName)) { - listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture); - currentRoot.delegatedNativeEvents.add(nativeFullName); + if (!currentRoot.delegatedNativeEvents[nativeFullName]) { + const listener = listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture); + currentRoot.delegatedNativeEvents[nativeFullName] = listener; } }); } diff --git a/libs/horizon/src/renderer/vnode/VNode.ts b/libs/horizon/src/renderer/vnode/VNode.ts index 80baedc9..03937e4a 100644 --- a/libs/horizon/src/renderer/vnode/VNode.ts +++ b/libs/horizon/src/renderer/vnode/VNode.ts @@ -78,7 +78,7 @@ export class VNode { // 根节点数据 toUpdateNodes: Set | null; // 保存要更新的节点 delegatedEvents: Set; - delegatedNativeEvents: Set; + delegatedNativeEvents: Record void>; belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode,处理ref的时候使用 @@ -100,7 +100,7 @@ export class VNode { this.task = null; this.toUpdateNodes = new Set(); this.delegatedEvents = new Set(); - this.delegatedNativeEvents = new Set(); + this.delegatedNativeEvents = {}; this.updates = null; this.stateCallbacks = null; this.state = null; From 46450cf4f8508339f80a34d8f7e4ab0e41d7f071 Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 5 Sep 2022 10:53:35 +0800 Subject: [PATCH 03/24] Match-id-fe440727969028d1d8e23d0315b8867e6ee4b5b7 --- libs/horizon/src/external/devtools.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/horizon/src/external/devtools.ts b/libs/horizon/src/external/devtools.ts index 0cc4d530..9e8295cb 100644 --- a/libs/horizon/src/external/devtools.ts +++ b/libs/horizon/src/external/devtools.ts @@ -39,7 +39,9 @@ export const helper = { return { name: HookName.RefHook, hIndex, value: (state as Ref).current }; } else if (isEffectHook(state)) { const name = - state.effectConstant == EffectConstant.LayoutEffect ? HookName.LayoutEffectHook : HookName.EffectHook; + state.effectConstant == EffectConstant.LayoutEffect || EffectConstant.LayoutEffect | EffectConstant.DepsChange + ? HookName.LayoutEffectHook + : HookName.EffectHook; return { name, hIndex, value: (state as Effect).effect }; } else if (isCallbackHook(state)) { return { name: HookName.CallbackHook, hIndex, value: (state as CallBack).func }; From 28c219de86bee7d33f4c8349e7dc166e5c4c8b1b Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 5 Sep 2022 11:15:07 +0800 Subject: [PATCH 04/24] Match-id-ffb38ef6e63f882eff4f7d1aa5cc7ca5181db8e5 --- libs/horizon/src/external/devtools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/horizon/src/external/devtools.ts b/libs/horizon/src/external/devtools.ts index 9e8295cb..1fb1af9b 100644 --- a/libs/horizon/src/external/devtools.ts +++ b/libs/horizon/src/external/devtools.ts @@ -39,7 +39,7 @@ export const helper = { return { name: HookName.RefHook, hIndex, value: (state as Ref).current }; } else if (isEffectHook(state)) { const name = - state.effectConstant == EffectConstant.LayoutEffect || EffectConstant.LayoutEffect | EffectConstant.DepsChange + state.effectConstant == EffectConstant.LayoutEffect || (EffectConstant.LayoutEffect | EffectConstant.DepsChange) ? HookName.LayoutEffectHook : HookName.EffectHook; return { name, hIndex, value: (state as Effect).effect }; From 0cfb5ce80a86d968494a07f1af8c585ff4a3a6af Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 5 Sep 2022 11:56:32 +0800 Subject: [PATCH 05/24] Match-id-fe726c014403321cf4f7f9cc4970dec88a84397c --- CHANGELOG.md | 3 +++ libs/horizon/package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4be6ff..648cfabd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.0.14 (2022-09-04) +- **core**: #44 修复unmount根节点事件未清除 + ## 0.0.13 (2022-08-02) - **horizonX**: 修复redux兼容器bug diff --git a/libs/horizon/package.json b/libs/horizon/package.json index b986d476..2e021729 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.13", + "version": "0.0.14", "homepage": "", "bugs": "", "main": "index.js", From e8be9638869cad4496ae56e738718261dfaf2053 Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 5 Sep 2022 17:23:34 +0800 Subject: [PATCH 06/24] Match-id-67aa6176d68afaa1fc27e819030181c8f9d889cb --- libs/horizon/src/dom/DOMExternal.ts | 9 +++-- libs/horizon/src/dom/DOMOperator.ts | 4 -- .../DOMPropertiesHandler.ts | 2 +- libs/horizon/src/event/EventBinding.ts | 28 +++++--------- libs/horizon/src/renderer/RootStack.ts | 17 +++++++++ libs/horizon/src/renderer/TreeBuilder.ts | 27 ++++++-------- libs/horizon/src/renderer/render/DomPortal.ts | 8 ++-- libs/horizon/src/renderer/vnode/VNode.ts | 3 +- .../ComponentTest/PortalComponent.test.js | 37 ++++++++++++++++++- 9 files changed, 84 insertions(+), 51 deletions(-) create mode 100644 libs/horizon/src/renderer/RootStack.ts diff --git a/libs/horizon/src/dom/DOMExternal.ts b/libs/horizon/src/dom/DOMExternal.ts index 6b82889f..f3054261 100644 --- a/libs/horizon/src/dom/DOMExternal.ts +++ b/libs/horizon/src/dom/DOMExternal.ts @@ -71,13 +71,14 @@ function findDOMNode(domOrEle?: Element): null | Element | Text { // 情况根节点监听器 function removeRootEventLister(container: Container) { - const root = container._treeRoot; - if (root) { - Object.keys(root.delegatedNativeEvents).forEach(event => { - const listener = root.delegatedNativeEvents[event]; + const events = (container._treeRoot as any).$EV; + if (events) { + Object.keys(events).forEach(event => { + const listener = events[event]; if (listener) { container.removeEventListener(event, listener); + events[event] = null; } }); } diff --git a/libs/horizon/src/dom/DOMOperator.ts b/libs/horizon/src/dom/DOMOperator.ts index 85b35da4..4e6b9c2a 100644 --- a/libs/horizon/src/dom/DOMOperator.ts +++ b/libs/horizon/src/dom/DOMOperator.ts @@ -225,7 +225,3 @@ export function unHideDom(tag: string, dom: Element | Text, props: Props) { dom.textContent = props; } } - -export function prePortal(portal: Element): void { - listenDelegatedEvents(portal); -} diff --git a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts index 7864426d..0d9dc259 100644 --- a/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts +++ b/libs/horizon/src/dom/DOMPropertiesHandler/DOMPropertiesHandler.ts @@ -3,7 +3,7 @@ import { updateCommonProp } from './UpdateCommonProp'; import { setStyles } from './StyleHandler'; import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding'; import { isEventProp } from '../validators/ValidateProps'; -import { getCurrentRoot } from '../../renderer/TreeBuilder'; +import { getCurrentRoot } from '../../renderer/RootStack'; // 初始化DOM属性和更新 DOM 属性 export function setDomProps(dom: Element, props: Object, isNativeTag: boolean, isInit: boolean): void { diff --git a/libs/horizon/src/event/EventBinding.ts b/libs/horizon/src/event/EventBinding.ts index 40e7625a..0623c9fb 100644 --- a/libs/horizon/src/event/EventBinding.ts +++ b/libs/horizon/src/event/EventBinding.ts @@ -48,22 +48,6 @@ function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, i return listener; } -// 监听所有委托事件 -export function listenDelegatedEvents(dom: Element) { - if (dom[listeningMarker]) { - // 不需要重复注册事件 - return; - } - dom[listeningMarker] = true; - - allDelegatedNativeEvents.forEach((nativeEvtName: string) => { - // 委托冒泡事件 - listenToNativeEvent(nativeEvtName, dom, false); - // 委托捕获事件 - listenToNativeEvent(nativeEvtName, dom, true); - }); -} - // 事件懒委托,当用户定义事件后,再进行委托到根节点 export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) { currentRoot.delegatedEvents.add(eventName); @@ -73,9 +57,17 @@ export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) { nativeEvents.forEach(nativeEvent => { const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent; - if (!currentRoot.delegatedNativeEvents[nativeFullName]) { + + // 事件存储在DOM节点属性,避免多个VNode(root和portal)对应同一个DOM, 造成事件重复监听 + let events = currentRoot.realNode.$EV; + + if (!events) { + events = (currentRoot.realNode as any).$EV = {}; + } + + if (!events[nativeFullName]) { const listener = listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture); - currentRoot.delegatedNativeEvents[nativeFullName] = listener; + events[nativeFullName] = listener; } }); } diff --git a/libs/horizon/src/renderer/RootStack.ts b/libs/horizon/src/renderer/RootStack.ts new file mode 100644 index 00000000..8bce59e6 --- /dev/null +++ b/libs/horizon/src/renderer/RootStack.ts @@ -0,0 +1,17 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved. + */ + +import { VNode } from './vnode/VNode'; +const currentRootStack: VNode[] = []; +export function getCurrentRoot() { + return currentRootStack[currentRootStack.length - 1]; +} + +export function pushCurrentRoot(root: VNode) { + return currentRootStack.push(root); +} + +export function popCurrentRoot() { + return currentRootStack.pop(); +} diff --git a/libs/horizon/src/renderer/TreeBuilder.ts b/libs/horizon/src/renderer/TreeBuilder.ts index 683c9bc3..7bc0eece 100644 --- a/libs/horizon/src/renderer/TreeBuilder.ts +++ b/libs/horizon/src/renderer/TreeBuilder.ts @@ -2,7 +2,7 @@ import type { VNode } from './Types'; import { callRenderQueueImmediate, pushRenderCallback } from './taskExecutor/RenderQueue'; import { updateVNode } from './vnode/VNodeCreator'; -import { TreeRoot, DomComponent, DomPortal } from './vnode/VNodeTags'; +import { DomComponent, DomPortal, TreeRoot } from './vnode/VNodeTags'; import { FlagUtils, InitFlag, Interrupted } from './vnode/VNodeFlags'; import { captureVNode } from './render/BaseComponent'; import { checkLoopingUpdateLimit, submitToRender } from './submit/Submit'; @@ -12,41 +12,39 @@ import componentRenders from './render'; import { BuildCompleted, BuildFatalErrored, - BuildInComplete, getBuildResult, + BuildInComplete, + getBuildResult, getStartVNode, setBuildResult, setProcessingClassVNode, - setStartVNode + setStartVNode, } from './GlobalVar'; import { ByAsync, BySync, - InRender, - InEvent, changeMode, checkMode, copyExecuteMode, + InEvent, + InRender, isExecuting, - setExecuteMode + setExecuteMode, } from './ExecuteMode'; -import { recoverParentContext, resetParentContext, resetNamespaceCtx, setNamespaceCtx } from './ContextSaver'; +import { recoverParentContext, resetNamespaceCtx, resetParentContext, setNamespaceCtx } from './ContextSaver'; import { updateChildShouldUpdate, updateParentsChildShouldUpdate, - updateShouldUpdateOfTree + updateShouldUpdateOfTree, } from './vnode/VNodeShouldUpdate'; import { getPathArr } from './utils/vNodePath'; import { injectUpdater } from '../external/devtools'; +import { popCurrentRoot, pushCurrentRoot } from './RootStack'; // 不可恢复错误 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; @@ -280,7 +278,7 @@ function buildVNodeTree(treeRoot: VNode) { // 总体任务入口 function renderFromRoot(treeRoot) { runAsyncEffects(); - currentRoot = treeRoot; + pushCurrentRoot(treeRoot); // 1. 构建vNode树 buildVNodeTree(treeRoot); @@ -291,8 +289,7 @@ function renderFromRoot(treeRoot) { // 2. 提交变更 submitToRender(treeRoot); - currentRoot = null; - + popCurrentRoot(); if (window.__HORIZON_DEV_HOOK__) { const hook = window.__HORIZON_DEV_HOOK__; // injector.js 可能在 Horizon 代码之后加载,此时无 __HORIZON_DEV_HOOK__ 全局变量 diff --git a/libs/horizon/src/renderer/render/DomPortal.ts b/libs/horizon/src/renderer/render/DomPortal.ts index baf4b500..719a5b60 100644 --- a/libs/horizon/src/renderer/render/DomPortal.ts +++ b/libs/horizon/src/renderer/render/DomPortal.ts @@ -1,18 +1,16 @@ import type { VNode } from '../Types'; import { resetNamespaceCtx, setNamespaceCtx } from '../ContextSaver'; import { createChildrenByDiff } from '../diff/nodeDiffComparator'; -import { prePortal } from '../../dom/DOMOperator'; +import { popCurrentRoot, pushCurrentRoot } from '../RootStack'; export function bubbleRender(processing: VNode) { resetNamespaceCtx(processing); - - if (processing.isCreated) { - prePortal(processing.realNode); - } + popCurrentRoot(); } function capturePortalComponent(processing: VNode) { setNamespaceCtx(processing, processing.realNode); + pushCurrentRoot(processing); const newElements = processing.props; if (processing.isCreated) { diff --git a/libs/horizon/src/renderer/vnode/VNode.ts b/libs/horizon/src/renderer/vnode/VNode.ts index 03937e4a..18b4d528 100644 --- a/libs/horizon/src/renderer/vnode/VNode.ts +++ b/libs/horizon/src/renderer/vnode/VNode.ts @@ -78,7 +78,6 @@ export class VNode { // 根节点数据 toUpdateNodes: Set | null; // 保存要更新的节点 delegatedEvents: Set; - delegatedNativeEvents: Record void>; belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode,处理ref的时候使用 @@ -100,7 +99,6 @@ export class VNode { this.task = null; this.toUpdateNodes = new Set(); this.delegatedEvents = new Set(); - this.delegatedNativeEvents = {}; this.updates = null; this.stateCallbacks = null; this.state = null; @@ -137,6 +135,7 @@ export class VNode { case DomPortal: this.realNode = null; this.context = null; + this.delegatedEvents = new Set(); this.src = null; break; case DomComponent: diff --git a/scripts/__tests__/ComponentTest/PortalComponent.test.js b/scripts/__tests__/ComponentTest/PortalComponent.test.js index 77811c2d..05908f18 100755 --- a/scripts/__tests__/ComponentTest/PortalComponent.test.js +++ b/scripts/__tests__/ComponentTest/PortalComponent.test.js @@ -3,7 +3,7 @@ import { getLogUtils } from '../jest/testUtils'; describe('PortalComponent Test', () => { const LogUtils = getLogUtils(); - + it('将子节点渲染到存在于父组件以外的 DOM 节点', () => { const portalRoot = document.createElement('div'); @@ -202,4 +202,37 @@ describe('PortalComponent Test', () => { 'bubble click event' ]); }); -}); \ No newline at end of file + + it('Create portal at app root should not add event listener multiple times', () => { + const btnRef = Horizon.createRef(); + class PortalApp extends Horizon.Component { + constructor(props) { + super(props); + } + + render() { + return Horizon.createPortal( + this.props.child, + container, + ); + } + } + const onClick = jest.fn(); + + class App extends Horizon.Component { + constructor(props) { + super(props); + } + + render() { + return
+ + +
; + } + } + Horizon.render(, container); + btnRef.current.click(); + expect(onClick).toHaveBeenCalledTimes(1); + }); +}); From 072ceecd137569322575db8cf4a521b7e03850dd Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 5 Sep 2022 19:42:05 +0800 Subject: [PATCH 07/24] Match-id-caaaaddbbc27d8d6fc7beb95bf9fc050a8f86051 --- libs/horizon/index.ts | 6 +- libs/horizon/src/horizonx/proxy/Observer.ts | 8 +- .../proxy/handlers/ArrayProxyHandler.ts | 18 +- .../proxy/handlers/CollectionProxyHandler.ts | 18 +- libs/horizon/src/horizonx/proxy/watch.ts | 14 +- .../src/horizonx/store/StoreHandler.ts | 16 +- .../src/renderer/render/BaseComponent.ts | 22 +- .../StoreFunctionality/watch.test.tsx | 228 +++++++++--------- .../edgeCases/multipleStores.test.tsx | 86 ++++--- 9 files changed, 199 insertions(+), 217 deletions(-) diff --git a/libs/horizon/index.ts b/libs/horizon/index.ts index f8035375..4c067a3e 100644 --- a/libs/horizon/index.ts +++ b/libs/horizon/index.ts @@ -25,7 +25,7 @@ import { useReducer, useRef, useState, - useDebugValue + useDebugValue, } from './src/renderer/hooks/HookExternal'; import { asyncUpdates } from './src/renderer/TreeBuilder'; import { callRenderQueueImmediate } from './src/renderer/taskExecutor/RenderQueue'; @@ -86,7 +86,7 @@ const Horizon = { useStore, clearStore, reduxAdapter, - watch + watch, }; export const version = __VERSION__; @@ -127,7 +127,7 @@ export { useStore, clearStore, reduxAdapter, - watch + watch, }; export default Horizon; diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index b00e942e..e35701af 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -7,7 +7,6 @@ import { launchUpdateFromVNode } from '../../renderer/TreeBuilder'; import { getProcessingVNode } from '../../renderer/GlobalVar'; import { VNode } from '../../renderer/vnode/VNode'; export interface IObserver { - useProp: (key: string) => void; addListener: (listener: () => void) => void; @@ -21,19 +20,18 @@ export interface IObserver { triggerUpdate: (vNode: any) => void; allChange: () => void; - + clearByVNode: (vNode: any) => void; } export class Observer implements IObserver { - vNodeKeys = new WeakMap(); keyVNodes = new Map(); - listeners:(()=>void)[] = []; + listeners: (() => void)[] = []; - watchers={} as {[key:string]:((key:string, oldValue:any, newValue:any)=>void)[]} + watchers = {} as { [key: string]: ((key: string, oldValue: any, newValue: any) => void)[] }; useProp(key: string | symbol): void { const processingVNode = getProcessingVNode(); diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index dd3fdb0b..391aee8c 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -12,18 +12,18 @@ export function createArrayProxy(rawObj: any[]): any[] { } function get(rawObj: any[], key: string, receiver: any) { - if (key === 'watch'){ + if (key === 'watch') { const observer = getObserver(rawObj); - return (prop:any, handler:(key:string, oldValue:any, newValue:any)=>void)=>{ - if(!observer.watchers[prop]){ - observer.watchers[prop]=[] as ((key:string, oldValue:any, newValue:any)=>void)[]; + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; } observer.watchers[prop].push(handler); - return ()=>{ - observer.watchers[prop]=observer.watchers[prop].filter(cb=>cb!==handler); - } - } + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; } if (isValidIntegerKey(key) || key === 'length') { @@ -43,7 +43,7 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { const observer = getObserver(rawObj); if (!isSame(newValue, oldValue)) { - if(observer.watchers?.[key]){ + if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { cb(key, oldValue, newValue); }); diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts index 15aaa682..99c3a26a 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -34,18 +34,18 @@ function get(rawObj: { size: number }, key: any, receiver: any): any { } else if (Object.prototype.hasOwnProperty.call(handler, key)) { const value = Reflect.get(handler, key, receiver); return value.bind(null, rawObj); - } else if (key === 'watch'){ + } else if (key === 'watch') { const observer = getObserver(rawObj); - return (prop:any, handler:(key:string, oldValue:any, newValue:any)=>void)=>{ - if(!observer.watchers[prop]){ - observer.watchers[prop]=[] as ((key:string, oldValue:any, newValue:any)=>void)[]; + return (prop: any, handler: (key: string, oldValue: any, newValue: any) => void) => { + if (!observer.watchers[prop]) { + observer.watchers[prop] = [] as ((key: string, oldValue: any, newValue: any) => void)[]; } observer.watchers[prop].push(handler); - return ()=>{ - observer.watchers[prop]=observer.watchers[prop].filter(cb=>cb!==handler); - } - } + return () => { + observer.watchers[prop] = observer.watchers[prop].filter(cb => cb !== handler); + }; + }; } return Reflect.get(rawObj, key, receiver); @@ -79,7 +79,7 @@ function set( } if (valChange) { - if(observer.watchers?.[key]){ + if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { cb(key, oldValue, newValue); }); diff --git a/libs/horizon/src/horizonx/proxy/watch.ts b/libs/horizon/src/horizonx/proxy/watch.ts index 79328142..11b65b9e 100644 --- a/libs/horizon/src/horizonx/proxy/watch.ts +++ b/libs/horizon/src/horizonx/proxy/watch.ts @@ -1,8 +1,8 @@ -export function watch(stateVariable:any,listener:(state:any)=>void){ - listener = listener.bind(null,stateVariable); - stateVariable.addListener(listener); +export function watch(stateVariable: any, listener: (state: any) => void) { + listener = listener.bind(null, stateVariable); + stateVariable.addListener(listener); - return ()=>{ - stateVariable.removeListener(listener); - } -} \ No newline at end of file + return () => { + stateVariable.removeListener(listener); + }; +} diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index 00d4695e..757a5b02 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -35,9 +35,7 @@ type StoreHandler, C extends UserComp $queue: QueuedStoreActions; $a: StoreActions; $c: UserComputedValues; -} & { [K in keyof S]: S[K] } & - { [K in keyof A]: Action } & - { [K in keyof C]: ReturnType }; +} & { [K in keyof S]: S[K] } & { [K in keyof A]: Action } & { [K in keyof C]: ReturnType }; type PlannedAction> = { action: string; @@ -103,7 +101,7 @@ export function createStore, C extend const $a: Partial> = {}; const $queue: Partial> = {}; const $c: Partial> = {}; - const handler = ({ + const handler = { $subscribe, $unsubscribe, $a: $a as StoreActions, @@ -111,7 +109,7 @@ export function createStore, C extend $c: $c as ComputedValues, $config: config, $queue: $queue as QueuedStoreActions, - } as unknown) as StoreHandler; + } as unknown as StoreHandler; function tryNextAction() { if (!plannedActions.length) { @@ -205,7 +203,7 @@ export function createStore, C extend } export function clearVNodeObservers(vNode) { - if(!vNode.observers) return; + if (!vNode.observers) return; vNode.observers.forEach(observer => { observer.clearByVNode(vNode); }); @@ -220,14 +218,14 @@ function hookStore() { if (!processingVNode) { return; } - + if (!processingVNode.observers) { processingVNode.observers = new Set(); } if (processingVNode.tag === FunctionComponent) { // from FunctionComponent - const vNodeRef = (useRef(null) as unknown) as { current: VNode }; + const vNodeRef = useRef(null) as unknown as { current: VNode }; vNodeRef.current = processingVNode; useEffect(() => { @@ -239,7 +237,7 @@ function hookStore() { } else if (processingVNode.tag === ClassComponent) { // from ClassComponent if (!processingVNode.classComponentWillUnmount) { - processingVNode.classComponentWillUnmount = function(vNode) { + processingVNode.classComponentWillUnmount = function (vNode) { clearVNodeObservers(vNode); vNode.observers = null; }; diff --git a/libs/horizon/src/renderer/render/BaseComponent.ts b/libs/horizon/src/renderer/render/BaseComponent.ts index 4192e100..e027696c 100644 --- a/libs/horizon/src/renderer/render/BaseComponent.ts +++ b/libs/horizon/src/renderer/render/BaseComponent.ts @@ -1,17 +1,11 @@ import type { VNode } from '../Types'; -import { - ContextProvider, - DomComponent, - DomPortal, - TreeRoot, - SuspenseComponent, -} from '../vnode/VNodeTags'; +import { ContextProvider, DomComponent, DomPortal, TreeRoot, SuspenseComponent } from '../vnode/VNodeTags'; import { setContext, setNamespaceCtx } from '../ContextSaver'; import { FlagUtils } from '../vnode/VNodeFlags'; -import {onlyUpdateChildVNodes} from '../vnode/VNodeCreator'; +import { onlyUpdateChildVNodes } from '../vnode/VNodeCreator'; import componentRenders from './index'; -import {setProcessingVNode} from '../GlobalVar'; +import { setProcessingVNode } from '../GlobalVar'; import { clearVNodeObservers } from '../../horizonx/store/StoreHandler'; // 复用vNode时,也需对stack进行处理 @@ -40,11 +34,7 @@ export function captureVNode(processing: VNode): VNode | null { if (processing.tag !== SuspenseComponent) { // 该vNode没有变化,不用进入capture,直接复用。 - if ( - !processing.isCreated && - processing.oldProps === processing.props && - !processing.shouldUpdate - ) { + if (!processing.isCreated && processing.oldProps === processing.props && !processing.shouldUpdate) { // 复用还需对stack进行处理 handlerContext(processing); @@ -56,8 +46,8 @@ export function captureVNode(processing: VNode): VNode | null { processing.shouldUpdate = false; setProcessingVNode(processing); - - if(processing.observers) clearVNodeObservers(processing); + + if (processing.observers) clearVNodeObservers(processing); const child = component.captureRender(processing, shouldUpdate); setProcessingVNode(null); diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx index 806c6029..d423f3bf 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/watch.test.tsx @@ -1,132 +1,130 @@ -import { createStore } from "@cloudsop/horizon/src/horizonx/store/StoreHandler"; -import { watch } from "@cloudsop/horizon/src/horizonx/proxy/watch"; +import { createStore } from '@cloudsop/horizon/src/horizonx/store/StoreHandler'; +import { watch } from '@cloudsop/horizon/src/horizonx/proxy/watch'; -describe("watch",()=>{ - it('shouhld watch promitive state variable', async()=>{ - const useStore = createStore({ - state:{ - variable:'x' - }, - actions:{ - change:(state)=>state.variable = "a" - } - }); - - const store = useStore(); - let counter = 0; - - watch(store.$s,(state)=>{ - counter++; - expect(state.variable).toBe('a'); - }) - - store.change(); - - expect(counter).toBe(1); - }); - it('shouhld watch object variable', async()=>{ - const useStore = createStore({ - state:{ - variable:'x' - }, - actions:{ - change:(state)=>state.variable = "a" - } - }); - - const store = useStore(); - let counter = 0; - - store.$s.watch('variable',()=>{ - counter++; - }) - - store.change(); - - expect(counter).toBe(1); +describe('watch', () => { + it('shouhld watch promitive state variable', async () => { + const useStore = createStore({ + state: { + variable: 'x', + }, + actions: { + change: state => (state.variable = 'a'), + }, }); - it('shouhld watch array item', async()=>{ - const useStore = createStore({ - state:{ - arr:['x'] - }, - actions:{ - change:(state)=>state.arr[0]='a' - } - }); + const store = useStore(); + let counter = 0; - const store = useStore(); - let counter = 0; - - store.arr.watch('0',()=>{ - counter++; - }) - - store.change(); - - expect(counter).toBe(1); + watch(store.$s, state => { + counter++; + expect(state.variable).toBe('a'); }); - it('shouhld watch collection item', async()=>{ - const useStore = createStore({ - state:{ - collection:new Map([ - ['a', 'a'], - ]) - }, - actions:{ - change:(state)=>state.collection.set('a','x') - } - }); + store.change(); - const store = useStore(); - let counter = 0; - - store.collection.watch('a',()=>{ - counter++; - }) - - store.change(); - - expect(counter).toBe(1); + expect(counter).toBe(1); + }); + it('shouhld watch object variable', async () => { + const useStore = createStore({ + state: { + variable: 'x', + }, + actions: { + change: state => (state.variable = 'a'), + }, }); - it('should watch multiple variables independedntly', async()=>{ - const useStore = createStore({ - state:{ - bool1:true, - bool2:false - }, - actions:{ - toggle1:state=>state.bool1=!state.bool1, - toggle2:state=>state.bool2=!state.bool2 - } - }); + const store = useStore(); + let counter = 0; - let counter1=0; - let counterAll=0; - const store = useStore(); + store.$s.watch('variable', () => { + counter++; + }); - watch(store.$s,()=>{ - counterAll++; - }) + store.change(); - store.$s.watch('bool1',()=>{ - counter1++; - }); + expect(counter).toBe(1); + }); - store.toggle1(); - store.toggle1(); + it('shouhld watch array item', async () => { + const useStore = createStore({ + state: { + arr: ['x'], + }, + actions: { + change: state => (state.arr[0] = 'a'), + }, + }); - store.toggle2(); + const store = useStore(); + let counter = 0; - store.toggle1(); + store.arr.watch('0', () => { + counter++; + }); - store.toggle2(); - store.toggle2(); + store.change(); - expect(counter1).toBe(3); - expect(counterAll).toBe(6); - }) -}) \ No newline at end of file + expect(counter).toBe(1); + }); + + it('shouhld watch collection item', async () => { + const useStore = createStore({ + state: { + collection: new Map([['a', 'a']]), + }, + actions: { + change: state => state.collection.set('a', 'x'), + }, + }); + + const store = useStore(); + let counter = 0; + + store.collection.watch('a', () => { + counter++; + }); + + store.change(); + + expect(counter).toBe(1); + }); + + it('should watch multiple variables independedntly', async () => { + const useStore = createStore({ + state: { + bool1: true, + bool2: false, + }, + actions: { + toggle1: state => (state.bool1 = !state.bool1), + toggle2: state => (state.bool2 = !state.bool2), + }, + }); + + let counter1 = 0; + let counterAll = 0; + const store = useStore(); + + watch(store.$s, () => { + counterAll++; + }); + + store.$s.watch('bool1', () => { + counter1++; + }); + + store.toggle1(); + store.toggle1(); + + store.toggle2(); + + store.toggle1(); + + store.toggle2(); + store.toggle2(); + + expect(counter1).toBe(3); + expect(counterAll).toBe(6); + }); +}); diff --git a/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx b/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx index 63fb74e5..9c9f847c 100644 --- a/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx +++ b/scripts/__tests__/HorizonXText/edgeCases/multipleStores.test.tsx @@ -6,20 +6,20 @@ import { describe, beforeEach, afterEach, it, expect } from '@jest/globals'; const { unmountComponentAtNode } = Horizon; const useStore1 = createStore({ - state:{ counter:1 }, - actions:{ - add:(state)=>state.counter++, - reset: (state)=>state.counter=1 - } -}) + state: { counter: 1 }, + actions: { + add: state => state.counter++, + reset: state => (state.counter = 1), + }, +}); const useStore2 = createStore({ - state:{ counter2:1 }, - actions:{ - add2:(state)=>state.counter2++, - reset: (state)=>state.counter2=1 - } -}) + state: { counter2: 1 }, + actions: { + add2: state => state.counter2++, + reset: state => (state.counter2 = 1), + }, +}); describe('Using multiple stores', () => { let container: HTMLElement | null = null; @@ -42,10 +42,10 @@ describe('Using multiple stores', () => { }); it('Should use multiple stores in class component', () => { - class App extends Horizon.Component{ - render(){ - const {counter,add} = useStore1(); - const {counter2, add2} = useStore2(); + class App extends Horizon.Component { + render() { + const { counter, add } = useStore1(); + const { counter2, add2 } = useStore2(); return (
@@ -65,9 +65,11 @@ describe('Using multiple stores', () => { > add -

{counter} {counter2}

+

+ {counter} {counter2} +

- ) + ); } } @@ -76,39 +78,36 @@ describe('Using multiple stores', () => { expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); - }); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID2); - }); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); - }); it('Should use use stores in cycles and multiple methods', () => { interface App { - store:any, - store2:any + store: any; + store2: any; } - class App extends Horizon.Component{ - constructor(){ + class App extends Horizon.Component { + constructor() { super(); this.store = useStore1(); - this.store2 = useStore2() + this.store2 = useStore2(); } - render(){ - const {counter,add} = useStore1(); + render() { + const { counter, add } = useStore1(); const store2 = useStore2(); - const {counter2, add2} = store2; + const { counter2, add2 } = store2; - for(let i=0; i<100; i++){ - const {counter,add} = useStore1(); + for (let i = 0; i < 100; i++) { + const { counter, add } = useStore1(); const store2 = useStore2(); - const {counter2, add2} = store2; + const { counter2, add2 } = store2; } return ( @@ -129,34 +128,33 @@ describe('Using multiple stores', () => { > add -

{counter} {counter2}

+

+ {counter} {counter2} +

- ) + ); } } Horizon.render(, container); - expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); + expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); - }); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID2); - }); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); - }); it('Should use multiple stores in function component', () => { function App() { - const {counter,add} = useStore1(); + const { counter, add } = useStore1(); const store2 = useStore2(); - const {counter2, add2} = store2; + const { counter2, add2 } = store2; return (
@@ -176,7 +174,9 @@ describe('Using multiple stores', () => { > add -

{counter} {counter2}

+

+ {counter} {counter2} +

); } @@ -186,14 +186,12 @@ describe('Using multiple stores', () => { expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID); - }); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1'); Horizon.act(() => { triggerClickEvent(container, BUTTON_ID2); - }); expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2'); }); -}); \ No newline at end of file +}); From 7b4a54c2e6de061c396af5e8cdf911500507effe Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 6 Sep 2022 14:43:50 +0800 Subject: [PATCH 08/24] Match-id-6c100f1d01da9e17e32d09176e40d5da6087ede5 --- libs/horizon/src/event/EventWrapper.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/libs/horizon/src/event/EventWrapper.ts b/libs/horizon/src/event/EventWrapper.ts index 84f5d844..9ca6aa9a 100644 --- a/libs/horizon/src/event/EventWrapper.ts +++ b/libs/horizon/src/event/EventWrapper.ts @@ -25,6 +25,9 @@ export class WrappedEvent { stopPropagation: () => void; preventDefault: () => void; + propagationStopped = false + isPropagationStopped = (): boolean => this.propagationStopped; + // 适配Keyboard键盘事件该函数不能由合成事件调用 getModifierState?: (keyArgs: string) => boolean; // 适配老版本事件api @@ -39,7 +42,11 @@ export class WrappedEvent { } } // stopPropagation和preventDefault 必须通过Event实例调用 - this.stopPropagation = () => nativeEvent.stopPropagation(); + this.stopPropagation = () => { + nativeEvent.stopPropagation(); + this.propagationStopped = true; + }; + this.preventDefault = () => nativeEvent.preventDefault(); // custom事件自定义属性 @@ -58,10 +65,6 @@ export class WrappedEvent { isDefaultPrevented(): boolean { return this.nativeEvent.defaultPrevented; } - - isPropagationStopped(): boolean { - return this.nativeEvent.cancelBubble; - } } // 创建普通自定义事件对象实例,和原生事件对应 From e6e3c70bb17c0e7779b7878d4d731fcc4183176a Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 6 Sep 2022 19:30:59 +0800 Subject: [PATCH 09/24] Match-id-33a3f50281392d8e556e44558fef235ef068ded3 --- CHANGELOG.md | 4 ++++ libs/horizon/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 648cfabd..f9bf3468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.15 (2022-09-06) +- **core**: #43 fix portal root 跟 app root重合时重复监听 +- **core**: #38 修复合成事件受普通事件stopPropagation影响无法触发 + ## 0.0.14 (2022-09-04) - **core**: #44 修复unmount根节点事件未清除 diff --git a/libs/horizon/package.json b/libs/horizon/package.json index 2e021729..5c492466 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.14", + "version": "0.0.15", "homepage": "", "bugs": "", "main": "index.js", From 5bc66a4e69c7de3c2ca53025c234f86275466d4a Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 6 Sep 2022 21:50:47 +0800 Subject: [PATCH 10/24] Match-id-78aab772cb8e654ef883d9b69084d2c4da273119 --- .../src/renderer/diff/nodeDiffComparator.ts | 5 +- .../ComponentTest/DiffAlgorithm.test.js | 48 +++++++++++++++++++ .../ComponentTest/HookTest/UseEffect.test.js | 2 +- 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 scripts/__tests__/ComponentTest/DiffAlgorithm.test.js diff --git a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts index 8e029fb6..9334116f 100644 --- a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts +++ b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts @@ -294,8 +294,9 @@ function diffArrayNodesHandler( if (rightOldIndex < 0 || rightOldNode === null) { break; } - - canBeReuse = checkCanReuseNode(rightOldNode, newChildren[rightIdx - 1]); + // 新旧节点的index应该相同才能复用,null会影响位置 + const samePosition = rightOldNode.eIndex === rightIdx - 1; + canBeReuse = checkCanReuseNode(rightOldNode, newChildren[rightIdx - 1]) && samePosition; // 不能复用,break if (!canBeReuse) { break; diff --git a/scripts/__tests__/ComponentTest/DiffAlgorithm.test.js b/scripts/__tests__/ComponentTest/DiffAlgorithm.test.js new file mode 100644 index 00000000..0f5ab14d --- /dev/null +++ b/scripts/__tests__/ComponentTest/DiffAlgorithm.test.js @@ -0,0 +1,48 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved. + */ + +import * as Horizon from '@cloudsop/horizon/index.ts'; + +describe('Diff Algorithm', () => { + it('null should diff correctly', () => { + const fn = jest.fn(); + + class C extends Horizon.Component { + constructor() { + super(); + fn(); + } + + render() { + return 1; + } + } + + let update; + + function App() { + const [current, setCurrent] = Horizon.useState(1); + update = setCurrent; + return ( + <> + {current === 1 ? : null} + {current === 2 ? : null} + {current === 3 ? : null} + + ); + } + + Horizon.render(, container); + expect(fn).toHaveBeenCalledTimes(1); + + update(2); + expect(fn).toHaveBeenCalledTimes(2); + + update(3); + expect(fn).toHaveBeenCalledTimes(3); + + update(1); + expect(fn).toHaveBeenCalledTimes(4); + }); +}); diff --git a/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js b/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js index a88cb813..443894a5 100644 --- a/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js +++ b/scripts/__tests__/ComponentTest/HookTest/UseEffect.test.js @@ -81,7 +81,7 @@ describe('useEffect Hook Test', () => { expect(LogUtils.getAndClear()).toEqual([]); // 在执行新的render前,会执行完上一次render的useEffect,所以LogUtils会加入'NewApp effect'。 Horizon.render([na], container); - expect(LogUtils.getAndClear()).toEqual(['NewApp effect']); + expect(LogUtils.getAndClear()).toEqual(['NewApp effect', 'NewApp']); expect(container.textContent).toBe('NewApp'); expect(LogUtils.getAndClear()).toEqual([]); }); From ec83aff5e62f1887f415093f3816bf458229ce79 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 7 Sep 2022 11:27:22 +0800 Subject: [PATCH 11/24] Match-id-91b148af957f0c2f4043f45a93a8a6e8929a0507 --- .../src/renderer/diff/nodeDiffComparator.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts index 9334116f..183e6dd9 100644 --- a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts +++ b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts @@ -55,7 +55,7 @@ function deleteVNodes(parentVNode: VNode, startDelVNode: VNode | null, endVNode? } } -function checkCanReuseNode(oldNode: VNode | null, newChild: any): boolean { +function checkCanReuseNode(oldNode: VNode | null, newChild: any, newNodeIdx: number): boolean { if (newChild === null) { return false; } @@ -70,7 +70,12 @@ function checkCanReuseNode(oldNode: VNode | null, newChild: any): boolean { return oldKey === null; } if (newChild.vtype === TYPE_COMMON_ELEMENT || newChild.vtype === TYPE_PORTAL) { - return oldKey === newChild.key; + if(oldKey || newChild.key) { + return oldKey === newChild.key; + } else { + // 新旧节点的index应该相同才能复用,null会影响位置 + return oldNode?.eIndex === newNodeIdx; + } } } @@ -254,7 +259,7 @@ function diffArrayNodesHandler( nextOldNode = oldNode.next; } - canBeReuse = checkCanReuseNode(oldNode, newChildren[leftIdx]); + canBeReuse = checkCanReuseNode(oldNode, newChildren[leftIdx], leftIdx); // 不能复用,break if (!canBeReuse) { oldNode = oldNode ?? nextOldNode; @@ -294,9 +299,8 @@ function diffArrayNodesHandler( if (rightOldIndex < 0 || rightOldNode === null) { break; } - // 新旧节点的index应该相同才能复用,null会影响位置 - const samePosition = rightOldNode.eIndex === rightIdx - 1; - canBeReuse = checkCanReuseNode(rightOldNode, newChildren[rightIdx - 1]) && samePosition; + + canBeReuse = checkCanReuseNode(rightOldNode, newChildren[rightIdx - 1], rightIdx - 1); // 不能复用,break if (!canBeReuse) { break; From b55f7c4f22c07490693682b50d9e5a70140a6e6d Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 7 Sep 2022 17:42:28 +0800 Subject: [PATCH 12/24] Match-id-e4196e9fd763be3e06cc2c404f9780e4664daff7 --- libs/horizon/src/renderer/diff/nodeDiffComparator.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts index 183e6dd9..84462e0e 100644 --- a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts +++ b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts @@ -70,7 +70,8 @@ function checkCanReuseNode(oldNode: VNode | null, newChild: any, newNodeIdx: num return oldKey === null; } if (newChild.vtype === TYPE_COMMON_ELEMENT || newChild.vtype === TYPE_PORTAL) { - if(oldKey || newChild.key) { + // key存在时用key判断复用 + if (oldKey != null || newChild.key != null) { return oldKey === newChild.key; } else { // 新旧节点的index应该相同才能复用,null会影响位置 From 4b62e8ee48b902f628827b895a5a8cf9f90a4a25 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 7 Sep 2022 17:52:26 +0800 Subject: [PATCH 13/24] Match-id-999bd213135d2e503b18dc8b65ad7e4c7f627294 --- libs/horizon/src/renderer/diff/nodeDiffComparator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts index 84462e0e..686b0692 100644 --- a/libs/horizon/src/renderer/diff/nodeDiffComparator.ts +++ b/libs/horizon/src/renderer/diff/nodeDiffComparator.ts @@ -71,7 +71,7 @@ function checkCanReuseNode(oldNode: VNode | null, newChild: any, newNodeIdx: num } if (newChild.vtype === TYPE_COMMON_ELEMENT || newChild.vtype === TYPE_PORTAL) { // key存在时用key判断复用 - if (oldKey != null || newChild.key != null) { + if (oldKey !== null || newChild.key !== null) { return oldKey === newChild.key; } else { // 新旧节点的index应该相同才能复用,null会影响位置 From 77be7a916af2517c78d74b8be6d7a3e404ef5049 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 7 Sep 2022 19:21:36 +0800 Subject: [PATCH 14/24] Match-id-177587a17105df0521475085affc471c406c53a8 --- CHANGELOG.md | 3 +++ libs/horizon/package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9bf3468..fe7fbdd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.0.16 (2022-09-07) +- **core**: #56,#65 diff null 不能正确卸载组件 + ## 0.0.15 (2022-09-06) - **core**: #43 fix portal root 跟 app root重合时重复监听 - **core**: #38 修复合成事件受普通事件stopPropagation影响无法触发 diff --git a/libs/horizon/package.json b/libs/horizon/package.json index 5c492466..b9b420bc 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.15", + "version": "0.0.16", "homepage": "", "bugs": "", "main": "index.js", From 9916cab3f731625d861233b4e5cef9ef77ccbf22 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 7 Sep 2022 21:09:50 +0800 Subject: [PATCH 15/24] Match-id-7bdd5aa7851d0fb3ba0271c01879051dea1ccf20 --- libs/horizon/src/renderer/TreeBuilder.ts | 7 ++++++- libs/horizon/src/renderer/vnode/VNode.ts | 2 ++ libs/horizon/src/renderer/vnode/VNodeUtils.ts | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/libs/horizon/src/renderer/TreeBuilder.ts b/libs/horizon/src/renderer/TreeBuilder.ts index 7bc0eece..2622bc4a 100644 --- a/libs/horizon/src/renderer/TreeBuilder.ts +++ b/libs/horizon/src/renderer/TreeBuilder.ts @@ -176,7 +176,12 @@ export function calcStartUpdateVNode(treeRoot: VNode) { } if (toUpdateNodes.length === 1) { - return toUpdateNodes[0]; + const toUpdateNode = toUpdateNodes[0]; + if (toUpdateNode.isCleared) { + return treeRoot; + } else { + return toUpdateNodes[0]; + } } // 要计算的节点过多,直接返回根节点 diff --git a/libs/horizon/src/renderer/vnode/VNode.ts b/libs/horizon/src/renderer/vnode/VNode.ts index 18b4d528..752c99cc 100644 --- a/libs/horizon/src/renderer/vnode/VNode.ts +++ b/libs/horizon/src/renderer/vnode/VNode.ts @@ -39,6 +39,8 @@ export class VNode { ref: RefType | ((handle: any) => void) | null = null; // 包裹一个函数,submit阶段使用,比如将外部useRef生成的对象赋值到ref上 oldProps: any = null; + // 是否已经被从树上移除 + isCleared = false; changeList: any; // DOM的变更列表 effectList: any[] | null; // useEffect 的更新数组 updates: any[] | null; // TreeRoot和ClassComponent使用的更新数组 diff --git a/libs/horizon/src/renderer/vnode/VNodeUtils.ts b/libs/horizon/src/renderer/vnode/VNodeUtils.ts index f3092976..789b8d2e 100644 --- a/libs/horizon/src/renderer/vnode/VNodeUtils.ts +++ b/libs/horizon/src/renderer/vnode/VNodeUtils.ts @@ -73,6 +73,7 @@ export function travelVNodeTree( // 置空vNode export function clearVNode(vNode: VNode) { + vNode.isCleared = true; vNode.child = null; vNode.next = null; vNode.depContexts = null; From c5549be36f9f581bb5182ef86bb3839681e50e5a Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 7 Sep 2022 21:39:00 +0800 Subject: [PATCH 16/24] Match-id-70d0aa42ebbc46bbc13f8c9c15f5fb16eba14cbe --- CHANGELOG.md | 3 +++ libs/horizon/package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe7fbdd9..5296cbeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.0.17 (2022-09-07) +- **core**: fix 不在树上的节点发起更新导致错误 + ## 0.0.16 (2022-09-07) - **core**: #56,#65 diff null 不能正确卸载组件 diff --git a/libs/horizon/package.json b/libs/horizon/package.json index b9b420bc..2bc5ecf0 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.16", + "version": "0.0.17", "homepage": "", "bugs": "", "main": "index.js", From df87d7d1a82c6df8573ffb963f37fdf7c9ea95aa Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 7 Sep 2022 22:17:12 +0800 Subject: [PATCH 17/24] Match-id-1e1386ee22959f5efdcccba603bbc688994eec6a --- libs/horizon/src/event/EventWrapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/horizon/src/event/EventWrapper.ts b/libs/horizon/src/event/EventWrapper.ts index 9ca6aa9a..37098a8e 100644 --- a/libs/horizon/src/event/EventWrapper.ts +++ b/libs/horizon/src/event/EventWrapper.ts @@ -58,7 +58,7 @@ export class WrappedEvent { this.type = nativeEvtName; // 兼容IE的event key - const orgKey = (nativeEvent as any).key; + const orgKey = (nativeEvent as any).key ?? ''; this.key = uniqueKeyMap.get(orgKey) || orgKey; } From 9f3f77e87df8905d2c2dfa5aede668b7680b74a1 Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 8 Sep 2022 16:32:47 +0800 Subject: [PATCH 18/24] Match-id-fb63f56fd45785b53724b05f91bb3e52d9386461 --- CHANGELOG.md | 3 +++ libs/horizon/package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5296cbeb..928e55bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.0.18 (2022-09-08) +- **core**: fix 键盘事件使用历史记录填充时key为undefined +- ## 0.0.17 (2022-09-07) - **core**: fix 不在树上的节点发起更新导致错误 diff --git a/libs/horizon/package.json b/libs/horizon/package.json index 2bc5ecf0..f59cb3f7 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.17", + "version": "0.0.18", "homepage": "", "bugs": "", "main": "index.js", From c5a275d6570e796858b8403cfab8ed0924b86917 Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 13 Sep 2022 16:29:26 +0800 Subject: [PATCH 19/24] Match-id-cf2539a89122b0c25a798cf4990d143902cb20dd --- .../ComponentTest/PortalComponent.test.js | 42 +++++++++++++++++++ .../__tests__/utils/dispatchChangeEvent.js | 9 ++++ 2 files changed, 51 insertions(+) create mode 100644 scripts/__tests__/utils/dispatchChangeEvent.js diff --git a/scripts/__tests__/ComponentTest/PortalComponent.test.js b/scripts/__tests__/ComponentTest/PortalComponent.test.js index 05908f18..5743093d 100755 --- a/scripts/__tests__/ComponentTest/PortalComponent.test.js +++ b/scripts/__tests__/ComponentTest/PortalComponent.test.js @@ -1,5 +1,6 @@ import * as Horizon from '@cloudsop/horizon/index.ts'; import { getLogUtils } from '../jest/testUtils'; +import dispatchChangeEvent from '../utils/dispatchChangeEvent'; describe('PortalComponent Test', () => { const LogUtils = getLogUtils(); @@ -235,4 +236,45 @@ describe('PortalComponent Test', () => { btnRef.current.click(); expect(onClick).toHaveBeenCalledTimes(1); }); + + it('Portal onChange should activate', () => { + class Dialog extends Horizon.Component { + node; + + constructor(props) { + super(props); + this.node = window.document.createElement('div'); + window.document.body.appendChild(this.node); + } +`` render() { + return Horizon.createPortal(this.props.children, this.node); + } + } + + let showPortalInput; + const fn = jest.fn(); + const inputRef = Horizon.createRef(); + function Input() { + const [show, setShow] = Horizon.useState(false); + showPortalInput = setShow; + + Horizon.useEffect(() => { + setTimeout(() => { + setShow(true); + }, 0); + }, []); + + if (!show) { + return null; + } + + return ; + } + + Horizon.render(
, container); + showPortalInput(true); + jest.advanceTimersToNextTimer(); + dispatchChangeEvent(inputRef.current, 'test'); + expect(fn).toHaveBeenCalledTimes(1); + }); }); diff --git a/scripts/__tests__/utils/dispatchChangeEvent.js b/scripts/__tests__/utils/dispatchChangeEvent.js new file mode 100644 index 00000000..1b8bbd04 --- /dev/null +++ b/scripts/__tests__/utils/dispatchChangeEvent.js @@ -0,0 +1,9 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved. + */ +export default function dispatchChangeEvent(inputEle, value) { + const nativeInputSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; + nativeInputSetter.call(inputEle, value); + + inputEle.dispatchEvent(new Event('input', { bubbles: true})); +} From 8e006f8cd26b46e59906694406f75657d2f2c84d Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 13 Sep 2022 16:36:55 +0800 Subject: [PATCH 20/24] Match-id-3d22079ded1ca2a343f8859618146c4dc7abe9a7 --- libs/horizon/src/dom/DOMExternal.ts | 2 +- libs/horizon/src/renderer/ContextSaver.ts | 30 ----- libs/horizon/src/renderer/TreeBuilder.ts | 46 ++++++- .../src/renderer/render/BaseComponent.ts | 8 +- .../ComponentTest/PortalComponent.test.js | 122 +++++++++--------- 5 files changed, 106 insertions(+), 102 deletions(-) diff --git a/libs/horizon/src/dom/DOMExternal.ts b/libs/horizon/src/dom/DOMExternal.ts index f3054261..0baa43c2 100644 --- a/libs/horizon/src/dom/DOMExternal.ts +++ b/libs/horizon/src/dom/DOMExternal.ts @@ -86,7 +86,7 @@ function removeRootEventLister(container: Container) { // 卸载入口 function destroy(container: Container): boolean { - if (container._treeRoot) { + if (container && container._treeRoot) { syncUpdates(() => { executeRender(null, container, () => { removeRootEventLister(container); diff --git a/libs/horizon/src/renderer/ContextSaver.ts b/libs/horizon/src/renderer/ContextSaver.ts index 2e72ce3c..e0ea8432 100644 --- a/libs/horizon/src/renderer/ContextSaver.ts +++ b/libs/horizon/src/renderer/ContextSaver.ts @@ -7,7 +7,6 @@ import type { VNode, ContextType } from './Types'; import type { Container } from '../dom/DOMOperator'; import { getNSCtx } from '../dom/DOMOperator'; -import { ContextProvider } from './vnode/VNodeTags'; // 保存的是“http://www.w3.org/1999/xhtml”或“http://www.w3.org/2000/svg”, // 用于识别是使用document.createElement()还是使用document.createElementNS()创建DOM @@ -44,32 +43,3 @@ export function resetContext(providerVNode: VNode) { context.value = providerVNode.context; } - -// 在局部更新时,从上到下恢复父节点的context -export function recoverParentContext(vNode: VNode) { - const contextProviders: VNode[] = []; - let parent = vNode.parent; - while (parent !== null) { - if (parent.tag === ContextProvider) { - contextProviders.unshift(parent); - } - parent = parent.parent; - } - contextProviders.forEach(node => { - setContext(node, node.props.value); - }); -} - -// 在局部更新时,从下到上重置父节点的context -export function resetParentContext(vNode: VNode) { - let parent = vNode.parent; - - while (parent !== null) { - if (parent.tag === ContextProvider) { - resetContext(parent); - } - parent = parent.parent; - } -} - - diff --git a/libs/horizon/src/renderer/TreeBuilder.ts b/libs/horizon/src/renderer/TreeBuilder.ts index 2622bc4a..d9a10bd7 100644 --- a/libs/horizon/src/renderer/TreeBuilder.ts +++ b/libs/horizon/src/renderer/TreeBuilder.ts @@ -2,7 +2,7 @@ import type { VNode } from './Types'; import { callRenderQueueImmediate, pushRenderCallback } from './taskExecutor/RenderQueue'; import { updateVNode } from './vnode/VNodeCreator'; -import { DomComponent, DomPortal, TreeRoot } from './vnode/VNodeTags'; +import { ContextProvider, DomComponent, DomPortal, TreeRoot } from './vnode/VNodeTags'; import { FlagUtils, InitFlag, Interrupted } from './vnode/VNodeFlags'; import { captureVNode } from './render/BaseComponent'; import { checkLoopingUpdateLimit, submitToRender } from './submit/Submit'; @@ -30,7 +30,12 @@ import { isExecuting, setExecuteMode, } from './ExecuteMode'; -import { recoverParentContext, resetNamespaceCtx, resetParentContext, setNamespaceCtx } from './ContextSaver'; +import { + resetContext, + resetNamespaceCtx, + setContext, + setNamespaceCtx, +} from './ContextSaver'; import { updateChildShouldUpdate, updateParentsChildShouldUpdate, @@ -244,7 +249,7 @@ function buildVNodeTree(treeRoot: VNode) { } // 恢复父节点的context - recoverParentContext(startVNode); + recoverTreeContext(startVNode); } // 重置环境变量,为重新进行深度遍历做准备 @@ -272,7 +277,7 @@ function buildVNodeTree(treeRoot: VNode) { } if (startVNode.tag !== TreeRoot) { // 不是根节点 // 恢复父节点的context - resetParentContext(startVNode); + resetTreeContext(startVNode); } setProcessingClassVNode(null); @@ -280,6 +285,39 @@ function buildVNodeTree(treeRoot: VNode) { setExecuteMode(preMode); } +// 在局部更新时,从上到下恢复父节点的context和PortalStack +function recoverTreeContext(vNode: VNode) { + const contextProviders: VNode[] = []; + let parent = vNode.parent; + while (parent !== null) { + if (parent.tag === ContextProvider) { + contextProviders.unshift(parent); + } + if(parent.tag === DomPortal){ + pushCurrentRoot(parent); + } + parent = parent.parent; + } + contextProviders.forEach(node => { + setContext(node, node.props.value); + }); +} + +// 在局部更新时,从下到上重置父节点的context +function resetTreeContext(vNode: VNode) { + let parent = vNode.parent; + + while (parent !== null) { + if (parent.tag === ContextProvider) { + resetContext(parent); + } + if(parent.tag === DomPortal){ + popCurrentRoot(); + } + parent = parent.parent; + } +} + // 总体任务入口 function renderFromRoot(treeRoot) { runAsyncEffects(); diff --git a/libs/horizon/src/renderer/render/BaseComponent.ts b/libs/horizon/src/renderer/render/BaseComponent.ts index e027696c..34ba6b6c 100644 --- a/libs/horizon/src/renderer/render/BaseComponent.ts +++ b/libs/horizon/src/renderer/render/BaseComponent.ts @@ -7,9 +7,10 @@ import { onlyUpdateChildVNodes } from '../vnode/VNodeCreator'; import componentRenders from './index'; import { setProcessingVNode } from '../GlobalVar'; import { clearVNodeObservers } from '../../horizonx/store/StoreHandler'; +import { pushCurrentRoot } from '../RootStack'; -// 复用vNode时,也需对stack进行处理 -function handlerContext(processing: VNode) { +// 复用vNode时,也需对树的上下文值处理,如context,portal, namespaceContext +function setTreeContextValue(processing: VNode) { switch (processing.tag) { case TreeRoot: setNamespaceCtx(processing, processing.realNode); @@ -19,6 +20,7 @@ function handlerContext(processing: VNode) { break; case DomPortal: setNamespaceCtx(processing, processing.realNode); + pushCurrentRoot(processing); break; case ContextProvider: { const newValue = processing.props.value; @@ -36,7 +38,7 @@ export function captureVNode(processing: VNode): VNode | null { // 该vNode没有变化,不用进入capture,直接复用。 if (!processing.isCreated && processing.oldProps === processing.props && !processing.shouldUpdate) { // 复用还需对stack进行处理 - handlerContext(processing); + setTreeContextValue(processing); return onlyUpdateChildVNodes(processing); } diff --git a/scripts/__tests__/ComponentTest/PortalComponent.test.js b/scripts/__tests__/ComponentTest/PortalComponent.test.js index 5743093d..2dcadc38 100755 --- a/scripts/__tests__/ComponentTest/PortalComponent.test.js +++ b/scripts/__tests__/ComponentTest/PortalComponent.test.js @@ -15,12 +15,10 @@ describe('PortalComponent Test', () => { } render() { - return Horizon.createPortal( - this.props.child, - this.element, - ); + return Horizon.createPortal(this.props.child, this.element); } } + Horizon.render(PortalApp
} />, container); expect(container.textContent).toBe(''); //
PortalApp
被渲染到了portalRoot而非container @@ -44,17 +42,12 @@ describe('PortalComponent Test', () => { render() { return [ - Horizon.createPortal( - this.props.child, - this.element, - ), - Horizon.createPortal( - this.props.child, - this.newElement, - ) + Horizon.createPortal(this.props.child, this.element), + Horizon.createPortal(this.props.child, this.newElement), ]; } } + Horizon.render(PortalApp} />, container); expect(container.textContent).toBe(''); //
PortalApp
被渲染到了portalRoot而非container @@ -83,21 +76,16 @@ describe('PortalComponent Test', () => { render() { return [
PortalApp1st
, - Horizon.createPortal([ -
PortalApp4
, - Horizon.createPortal( - this.props.child, - this.element3rd, - ), - ], this.element), -
PortalApp2nd
, Horizon.createPortal( - this.props.child, - this.newElement, - ) + [
PortalApp4
, Horizon.createPortal(this.props.child, this.element3rd)], + this.element + ), +
PortalApp2nd
, + Horizon.createPortal(this.props.child, this.newElement), ]; } } + Horizon.render(PortalApp} />, container); expect(container.textContent).toBe('PortalApp1stPortalApp2nd'); //
PortalApp4
会挂载在this.element上 @@ -121,25 +109,23 @@ describe('PortalComponent Test', () => { } render() { - return Horizon.createPortal( - this.props.child, - this.element, - ); + return Horizon.createPortal(this.props.child, this.element); } } - Horizon.render(PortalApp} />, container); + + Horizon.render(PortalApp} />, container); expect(container.textContent).toBe(''); expect(portalRoot.textContent).toBe('PortalApp'); - Horizon.render(AppPortal} />, container); + Horizon.render(AppPortal} />, container); expect(container.textContent).toBe(''); expect(portalRoot.textContent).toBe('AppPortal'); - Horizon.render(, container); + Horizon.render(, container); expect(container.textContent).toBe(''); expect(portalRoot.textContent).toBe('portal'); - Horizon.render(, container); + Horizon.render(, container); expect(container.textContent).toBe(''); expect(portalRoot.textContent).toBe(''); @@ -159,10 +145,7 @@ describe('PortalComponent Test', () => { } render() { - return Horizon.createPortal( - this.props.child, - this.element, - ); + return Horizon.createPortal(this.props.child, this.element); } } @@ -174,7 +157,6 @@ describe('PortalComponent Test', () => { ); }; - const App = () => { const handleClick = () => { LogUtils.log('bubble click event'); @@ -186,9 +168,7 @@ describe('PortalComponent Test', () => { return (
- }> - - + }>
); }; @@ -200,24 +180,23 @@ describe('PortalComponent Test', () => { expect(LogUtils.getAndClear()).toEqual([ // 从外到内先捕获再冒泡 'capture click event', - 'bubble click event' + 'bubble click event', ]); }); it('Create portal at app root should not add event listener multiple times', () => { - const btnRef = Horizon.createRef(); + const btnRef = Horizon.createRef(); + class PortalApp extends Horizon.Component { constructor(props) { super(props); } render() { - return Horizon.createPortal( - this.props.child, - container, - ); + return Horizon.createPortal(this.props.child, container); } } + const onClick = jest.fn(); class App extends Horizon.Component { @@ -226,18 +205,21 @@ describe('PortalComponent Test', () => { } render() { - return
- - -
; + return ( +
+ + +
+ ); } } + Horizon.render(, container); btnRef.current.click(); expect(onClick).toHaveBeenCalledTimes(1); }); - it('Portal onChange should activate', () => { + it('#76 Portal onChange should activate', () => { class Dialog extends Horizon.Component { node; @@ -246,7 +228,8 @@ describe('PortalComponent Test', () => { this.node = window.document.createElement('div'); window.document.body.appendChild(this.node); } -`` render() { + + render() { return Horizon.createPortal(this.props.children, this.node); } } @@ -254,24 +237,35 @@ describe('PortalComponent Test', () => { let showPortalInput; const fn = jest.fn(); const inputRef = Horizon.createRef(); - function Input() { - const [show, setShow] = Horizon.useState(false); - showPortalInput = setShow; - Horizon.useEffect(() => { - setTimeout(() => { - setShow(true); - }, 0); - }, []); + function App() { + const Input = () => { + const [show, setShow] = Horizon.useState(false); + showPortalInput = setShow; - if (!show) { - return null; - } + Horizon.useEffect(() => { + setTimeout(() => { + setShow(true); + }, 0); + }, []); - return ; + if (!show) { + return null; + } + + return ; + }; + + return ( +
+ + + +
+ ); } - Horizon.render(
, container); + Horizon.render(, container); showPortalInput(true); jest.advanceTimersToNextTimer(); dispatchChangeEvent(inputRef.current, 'test'); From 2e69a72c97ff6e3e99dc35a3ade8081d410f15aa Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 13 Sep 2022 20:22:38 +0800 Subject: [PATCH 21/24] Match-id-9d21c4b91a4dd45141621f3e3aa091806644a334 --- CHANGELOG.md | 5 ++++- libs/horizon/package.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 928e55bd..2f72d4a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ +## 0.0.19 (2022-09-13) +- **core**: fix 弹窗的input可能无法触发onChange事件 +- ## 0.0.18 (2022-09-08) - **core**: fix 键盘事件使用历史记录填充时key为undefined -- + ## 0.0.17 (2022-09-07) - **core**: fix 不在树上的节点发起更新导致错误 diff --git a/libs/horizon/package.json b/libs/horizon/package.json index f59cb3f7..f5aa0f58 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.18", + "version": "0.0.19", "homepage": "", "bugs": "", "main": "index.js", From 0b11905b3561b3d6b293bedbe3b433a9949888c1 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 14 Sep 2022 14:51:37 +0800 Subject: [PATCH 22/24] Match-id-a259fe636f4eae5da9d6448d48172e8ce9dcfcee --- .../src/renderer/vnode/VNodeCreator.ts | 2 +- scripts/__tests__/ComponentTest/Memo.test.js | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 scripts/__tests__/ComponentTest/Memo.test.js diff --git a/libs/horizon/src/renderer/vnode/VNodeCreator.ts b/libs/horizon/src/renderer/vnode/VNodeCreator.ts index 79496323..6416a7b7 100644 --- a/libs/horizon/src/renderer/vnode/VNodeCreator.ts +++ b/libs/horizon/src/renderer/vnode/VNodeCreator.ts @@ -230,7 +230,7 @@ export function onlyUpdateChildVNodes(processing: VNode): VNode | null { } }; - putChildrenIntoQueue(processing.child); + putChildrenIntoQueue(processing); while (queue.length) { const vNode = queue.shift()!; diff --git a/scripts/__tests__/ComponentTest/Memo.test.js b/scripts/__tests__/ComponentTest/Memo.test.js new file mode 100644 index 00000000..b1a6fa9f --- /dev/null +++ b/scripts/__tests__/ComponentTest/Memo.test.js @@ -0,0 +1,41 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved. + */ + +import * as Horizon from '@cloudsop/horizon/index.ts'; + +describe('Memo Test', () => { + it('Memo should not make the path wrong', function () { + let updateApp; + + function Child() { + const [_, update] = Horizon.useState({}); + updateApp = () => update({}); + return
; + } + const MemoChild = Horizon.memo(Child); + + function App() { + return ( +
+ +
+ ); + } + const MemoApp = Horizon.memo(App); + Horizon.render( +
+ +
, + container + ); + Horizon.render( +
+ + +
, + container + ); + expect(() => updateApp()).not.toThrow(); + }); +}); From 3a7599f943c149e2ea0a641445c7c21b246dd4f9 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 14 Sep 2022 15:04:34 +0800 Subject: [PATCH 23/24] Match-id-37ca0b38aea11de7f9b6bb8a0793b9fcb1d6966b --- CHANGELOG.md | 5 ++++- libs/horizon/package.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f72d4a1..9a01913e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ +## 0.0.20 (2022-09-14) +- **core**: #81 fix Memo场景路径错误 + ## 0.0.19 (2022-09-13) - **core**: fix 弹窗的input可能无法触发onChange事件 -- + ## 0.0.18 (2022-09-08) - **core**: fix 键盘事件使用历史记录填充时key为undefined diff --git a/libs/horizon/package.json b/libs/horizon/package.json index f5aa0f58..83d12372 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.19", + "version": "0.0.20", "homepage": "", "bugs": "", "main": "index.js", From 5f3f527cc2289eb32796d7e2d2ad92167c2a3076 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 14 Sep 2022 22:03:43 +0800 Subject: [PATCH 24/24] Match-id-0c4b6d0fb66f9c58533baea757dfb550720feb91 --- libs/horizon/src/horizonx/adapters/redux.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/horizon/src/horizonx/adapters/redux.ts b/libs/horizon/src/horizonx/adapters/redux.ts index fad3a6ef..11617f5d 100644 --- a/libs/horizon/src/horizonx/adapters/redux.ts +++ b/libs/horizon/src/horizonx/adapters/redux.ts @@ -127,7 +127,7 @@ export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dis return boundActionCreators; } -export function compose(middlewares: ReduxMiddleware[]) { +export function compose(...middlewares: ReduxMiddleware[]) { return (store: ReduxStoreHandler, extraArgument: any) => { let val; middlewares.reverse().forEach((middleware: ReduxMiddleware, index) => {