Skip to content

Commit

Permalink
feat: lazy creating reducer (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
geekact committed Dec 16, 2022
1 parent e574d84 commit 462bc77
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 17 deletions.
13 changes: 9 additions & 4 deletions src/model/defineModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
import { isFunction } from '../utils/isType';
import { Unsubscribe } from 'redux';
import { freeze, original, isDraft } from 'immer';
import { lazyLoad } from './lazyLoad';

export const defineModel = <
Name extends string,
Expand Down Expand Up @@ -103,6 +104,7 @@ export const defineModel = <

const getState = <T extends object>(obj: T): T & GetState<State> => {
return defineGetter(obj, 'state', () => {
lazyLoad(uniqueName);
const state = modelStore.getState()[uniqueName];
return depsCollector.active
? new ObjectDeps(modelStore, uniqueName).start(state)
Expand Down Expand Up @@ -218,15 +220,17 @@ export const defineModel = <
}
}

if (events) {
const onLazyLoaded = () => {
if (!events) return;

const { onInit, onChange, onDestroy } = events;
const eventCtx: EventCtx<State> = Object.assign(
composeGetter({ name: uniqueName }, getState),
enhancedMethods.external,
enhancedMethods.internal,
);

modelStore.onInitialized().then(() => {
{
const subscriptions: Unsubscribe[] = [];

if (onChange) {
Expand Down Expand Up @@ -258,8 +262,8 @@ export const defineModel = <
}

onInit && onInit.call(eventCtx);
});
}
}
};

ModelStore.appendReducer.call(
modelStore,
Expand All @@ -269,6 +273,7 @@ export const defineModel = <
initialState,
allowRefresh: !skipRefresh,
}),
onLazyLoaded,
);

const model: InternalModel<Name, State, Action, Effect, Computed> =
Expand Down
2 changes: 2 additions & 0 deletions src/model/enhanceAction.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { PreModelAction } from '../actions/model';
import { modelStore } from '../store/modelStore';
import { toArgs } from '../utils/toArgs';
import { lazyLoad } from './lazyLoad';
import type { ActionCtx } from './types';

export interface EnhancedAction<State extends object> {
Expand All @@ -26,6 +27,7 @@ export const enhanceAction = <State extends object>(
};

const fn: EnhancedAction<State> = function () {
lazyLoad(modelName);
return modelStore.dispatch<PreModelAction<State, any[]>>({
type: actionType,
model: modelName,
Expand Down
5 changes: 5 additions & 0 deletions src/model/lazyLoad.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { modelStore, ModelStore } from '../store/modelStore';

export function lazyLoad(modelName: string): void {
ModelStore.lazyLoad.call(modelStore, modelName);
}
5 changes: 4 additions & 1 deletion src/model/useDefined.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DestroyLodingAction, DESTROY_LOADING } from '../actions/loading';
import { loadingStore } from '../store/loadingStore';
import { ModelStore, modelStore } from '../store/modelStore';
import { cloneModel } from './cloneModel';
import { lazyLoad } from './lazyLoad';
import { HookModel as HookModel, Model } from './types';

let nameCounter = 0;
Expand All @@ -25,7 +26,9 @@ export const useDefined = <
: useDevName(modelName, initialCount, new Error());

const hookModel = useMemo(() => {
return cloneModel(uniqueName, globalModel);
const model = cloneModel(uniqueName, globalModel);
lazyLoad(uniqueName);
return model;
}, [uniqueName]);

return hookModel as any;
Expand Down
5 changes: 4 additions & 1 deletion src/model/useModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { HookModel, Model } from './types';
import { toArgs } from '../utils/toArgs';
import { useModelSelector } from '../redux/useSelector';
import { isFunction, isString } from '../utils/isType';
import { lazyLoad } from './lazyLoad';

/**
* hooks新旧数据的对比方式:
Expand Down Expand Up @@ -212,7 +213,9 @@ export function useModel(): any {

const reducerNames: string[] = [];
for (i = 0; i < modelsLength; ++i) {
reducerNames.push(models[i]!.name);
const modelName = models[i]!.name;
lazyLoad(modelName);
reducerNames.push(modelName);
}

return useModelSelector((state: Record<string, object>) => {
Expand Down
2 changes: 2 additions & 0 deletions src/persist/PersistItem.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { StorageEngine } from '../engines';
import { lazyLoad } from '../model/lazyLoad';
import type { InternalModel, Model, ModelPersist } from '../model/types';
import { isObject, isString } from '../utils/isType';
import { parseState, stringifyState } from '../utils/serialize';
Expand Down Expand Up @@ -96,6 +97,7 @@ export class PersistItem {
this.key = keyPrefix + key;

models.forEach((model) => {
lazyLoad(model.name);
const {
decode = defaultDecodeFn,
maxAge: customMaxAge = maxAge,
Expand Down
41 changes: 33 additions & 8 deletions src/store/modelStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface CreateStoreOptions {
persist?: PersistOptions[];
}

export class ModelStore extends StoreBasic<Record<string, any>> {
export class ModelStore<T = Record<string, any>> extends StoreBasic<T> {
public topic: Topic<{
init: [];
ready: [];
Expand All @@ -38,6 +38,10 @@ export class ModelStore extends StoreBasic<Record<string, any>> {
protected _isReady: boolean = false;
protected consumers: Record<string, Reducer> = {};
protected reducerKeys: string[] = [];
protected lazyKeys: Record<
string,
{ consumer: Reducer; onComplete: () => void }
> = {};
/**
* @protected
*/
Expand Down Expand Up @@ -125,12 +129,16 @@ export class ModelStore extends StoreBasic<Record<string, any>> {
this.topic.publish('unmount');
}

onInitialized(): Promise<void> {
onInitialized(maybeSync?: () => void): Promise<void> {
return new Promise((resolve) => {
if (this._isReady) {
maybeSync?.();
resolve();
} else {
this.topic.subscribeOnce('ready', resolve);
this.topic.subscribeOnce('ready', () => {
maybeSync?.();
resolve();
});
}
});
}
Expand Down Expand Up @@ -189,11 +197,7 @@ export class ModelStore extends StoreBasic<Record<string, any>> {
};
}

public static appendReducer(
this: ModelStore,
key: string,
consumer: Reducer,
): void {
protected appendReducer(key: string, consumer: Reducer) {
const store = this.origin;
const consumers = this.consumers;
const exists = store && consumers.hasOwnProperty(key);
Expand All @@ -203,6 +207,27 @@ export class ModelStore extends StoreBasic<Record<string, any>> {
store && !exists && store.replaceReducer(this.reducer);
}

public static appendReducer(
this: ModelStore,
key: string,
consumer: Reducer,
onComplete: () => void,
): void {
this.lazyKeys[key] = {
consumer,
onComplete,
};
}

public static lazyLoad(this: ModelStore, key: string) {
if (this.lazyKeys.hasOwnProperty(key)) {
const { consumer, onComplete } = this.lazyKeys[key]!;
delete this.lazyKeys[key];
this.appendReducer(key, consumer);
this.onInitialized(onComplete);
}
}

public static removeReducer(this: ModelStore, key: string): void {
const store = this.origin;
const consumers = this.consumers;
Expand Down
88 changes: 88 additions & 0 deletions test/lazyLoad.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { defineModel, store } from '../src';
import { lazyLoad } from '../src/model/lazyLoad';
import { ModelStore } from '../src/store/modelStore';
import { basicModel } from './models/basicModel';

beforeEach(() => {
store.init();
});

afterEach(() => {
store.unmount();
});

test('reducer is not in store by default', () => {
expect(store.getState()).not.toHaveProperty(basicModel.name);
});

test('reducer is in store by access state', () => {
basicModel.state;
expect(store.getState()).toHaveProperty(basicModel.name);
});

test('reducer is in store by calling reducers', () => {
basicModel.plus(1);
expect(store.getState()).toHaveProperty(basicModel.name);
});

test('reducer is in store by manual calling lazyLoad', () => {
lazyLoad(basicModel.name);
expect(store.getState()).toHaveProperty(basicModel.name);
});

test('events are called once reducer is loaded', async () => {
const initFn = jest.fn();
const changeFn = jest.fn();
const unmountFn = jest.fn();

const model = defineModel('event' + Math.random(), {
initialState: {
count: 0,
},
reducers: {
change(state) {
state.count++;
},
},
events: {
onInit: initFn,
onChange: changeFn,
onDestroy: unmountFn,
},
});

await store.onInitialized();

expect(initFn).toBeCalledTimes(0);
expect(changeFn).toBeCalledTimes(0);
expect(unmountFn).toBeCalledTimes(0);

model.state;
expect(initFn).toBeCalledTimes(1);
expect(changeFn).toBeCalledTimes(0);
expect(unmountFn).toBeCalledTimes(0);

model.change();
expect(initFn).toBeCalledTimes(1);
expect(changeFn).toBeCalledTimes(1);
expect(unmountFn).toBeCalledTimes(0);

model.change();
expect(initFn).toBeCalledTimes(1);
expect(changeFn).toBeCalledTimes(2);
expect(unmountFn).toBeCalledTimes(0);

ModelStore.removeReducer.call(store, model.name);
expect(initFn).toBeCalledTimes(1);
expect(changeFn).toBeCalledTimes(2);
expect(unmountFn).toBeCalledTimes(1);
});

test('never load reducer again since it had been destroyed', () => {
lazyLoad(basicModel.name);
expect(store.getState()).toHaveProperty(basicModel.name);

ModelStore.removeReducer.call(store, basicModel.name);
lazyLoad(basicModel.name);
expect(store.getState()).not.toHaveProperty(basicModel.name);
});
5 changes: 2 additions & 3 deletions test/lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cloneModel, defineModel, engines, store } from '../src';
import { lazyLoad } from '../src/model/lazyLoad';
import { PersistSchema } from '../src/persist/PersistItem';
import { ModelStore } from '../src/store/modelStore';

Expand Down Expand Up @@ -121,10 +122,8 @@ describe('onChange', () => {
},
},
});
model.plus();
model.minus();
expect(testMessage).toBe('');

lazyLoad(model.name);
await store.onInitialized();

expect(testMessage).toBe('onInit-prev-0-next-2-');
Expand Down
3 changes: 3 additions & 0 deletions test/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineModel, getLoading, store } from '../src';
import { DestroyLodingAction, DESTROY_LOADING } from '../src/actions/loading';
import { lazyLoad } from '../src/model/lazyLoad';
import { loadingStore } from '../src/store/loadingStore';
import { basicModel } from './models/basicModel';
import { complexModel } from './models/complexModel';
Expand All @@ -14,6 +15,8 @@ afterEach(() => {

test('dispatch the same state should be intercepted', () => {
const fn = jest.fn();
lazyLoad(basicModel.name);
lazyLoad(complexModel.name);
const unsubscribe = store.subscribe(fn);

expect(fn).toHaveBeenCalledTimes(0);
Expand Down

0 comments on commit 462bc77

Please sign in to comment.