From 93f529b55e565f61702c98ab4ee5283eccb14a99 Mon Sep 17 00:00:00 2001 From: * <8> Date: Wed, 31 Aug 2022 17:06:27 +0800 Subject: [PATCH 1/2] 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 072ceecd137569322575db8cf4a521b7e03850dd Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 5 Sep 2022 19:42:05 +0800 Subject: [PATCH 2/2] 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 +});