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 +});