Match-id-1ba61a1afe0d2bd9e6e730e66daf9f39970e4499
This commit is contained in:
parent
0759f07766
commit
93f529b55e
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -204,7 +204,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);
|
||||
});
|
||||
|
@ -219,11 +220,8 @@ function hookStore() {
|
|||
if (!processingVNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (processingVNode.observers) {
|
||||
// 清除上一次缓存的Observer依赖
|
||||
clearVNodeObservers(processingVNode);
|
||||
} else {
|
||||
|
||||
if (!processingVNode.observers) {
|
||||
processingVNode.observers = new Set<Observer>();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
})
|
|
@ -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 (
|
||||
<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');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue