Match-id-1f8ac0bd579cd4306e428c72ff7dad1408ffd568

This commit is contained in:
* 2022-09-05 19:52:28 +08:00 committed by *
commit a405141efa
10 changed files with 413 additions and 31 deletions

View File

@ -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';
@ -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;

View File

@ -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,17 +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)[] };
useProp(key: string | symbol): void {
const processingVNode = getProcessingVNode();

View File

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

View File

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

View File

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

View File

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

View File

@ -35,9 +35,7 @@ type StoreHandler<S extends object, A extends UserActions<S>, C extends UserComp
$queue: QueuedStoreActions<S, A>;
$a: StoreActions<S, A>;
$c: UserComputedValues<S>;
} & { [K in keyof S]: S[K] } &
{ [K in keyof A]: Action<A[K], S> } &
{ [K in keyof C]: ReturnType<C[K]> };
} & { [K in keyof S]: S[K] } & { [K in keyof A]: Action<A[K], S> } & { [K in keyof C]: ReturnType<C[K]> };
type PlannedAction<S extends object, F extends ActionFunction<S>> = {
action: string;
@ -103,7 +101,7 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
const $a: Partial<StoreActions<S, A>> = {};
const $queue: Partial<StoreActions<S, A>> = {};
const $c: Partial<ComputedValues<S, C>> = {};
const handler = ({
const handler = {
$subscribe,
$unsubscribe,
$a: $a as StoreActions<S, A>,
@ -111,7 +109,7 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
$c: $c as ComputedValues<S, C>,
$config: config,
$queue: $queue as QueuedStoreActions<S, A>,
} as unknown) as StoreHandler<S, A, C>;
} as unknown as StoreHandler<S, A, C>;
function tryNextAction() {
if (!plannedActions.length) {
@ -204,7 +202,8 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
return createStoreHook(handler);
}
function clearVNodeObservers(vNode) {
export function clearVNodeObservers(vNode) {
if (!vNode.observers) return;
vNode.observers.forEach(observer => {
observer.clearByVNode(vNode);
});
@ -220,16 +219,13 @@ function hookStore() {
return;
}
if (processingVNode.observers) {
// 清除上一次缓存的Observer依赖
clearVNodeObservers(processingVNode);
} else {
if (!processingVNode.observers) {
processingVNode.observers = new Set<Observer>();
}
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(() => {
@ -241,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;
};

View File

@ -1,17 +1,12 @@
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进行处理
function handlerContext(processing: VNode) {
@ -39,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);
@ -55,6 +46,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);

View File

@ -0,0 +1,130 @@
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);
});
});

View File

@ -0,0 +1,197 @@
//@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 (
<div>
<button
id={BUTTON_ID}
onClick={() => {
add();
}}
>
add
</button>
<button
id={BUTTON_ID2}
onClick={() => {
add2();
}}
>
add
</button>
<p id={RESULT_ID}>
{counter} {counter2}
</p>
</div>
);
}
}
Horizon.render(<App />, 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 (
<div>
<button
id={BUTTON_ID}
onClick={() => {
add();
}}
>
add
</button>
<button
id={BUTTON_ID2}
onClick={() => {
this.store2.add2();
}}
>
add
</button>
<p id={RESULT_ID}>
{counter} {counter2}
</p>
</div>
);
}
}
Horizon.render(<App />, 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 (
<div>
<button
id={BUTTON_ID}
onClick={() => {
add();
}}
>
add
</button>
<button
id={BUTTON_ID2}
onClick={() => {
add2();
}}
>
add
</button>
<p id={RESULT_ID}>
{counter} {counter2}
</p>
</div>
);
}
Horizon.render(<App />, 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');
});
});