@turbox3d/reactivity
Version:
Large-scale reactive state management library
330 lines (290 loc) • 9.6 kB
text/typescript
import { nextTick, includes, isPromise, batchRemoveFromSet } from '@turbox3d/shared';
import { Store, DispatchedAction, Mutation } from '../interfaces';
import { ReactionId, triggerCollector } from './collector';
import { EMaterialType } from '../const/enums';
import { Reaction } from './reactive';
import { ctx } from '../const/config';
import { AfterStoreChangeEvent, BeforeStoreChangeEvent, EEventName, emitter } from '../utils/event';
import { materialCallStack } from '../utils/materialCallStack';
import { TimeTravel } from './time-travel';
export let store: Store;
export interface ActionType {
name: string;
displayName: string;
}
export const actionTypeChain: ActionType[] = [];
export interface RegisterExternalBatchUpdateParam {
handler: (callback: () => void, finish?: () => void) => void | Promise<void>;
idCustomType: string;
}
export interface IdToListenersInfo {
listeners: Function[];
idCustomType?: string;
}
const batchedUpdates: RegisterExternalBatchUpdateParam[] = [];
export const registerExternalBatchUpdate = (obj: RegisterExternalBatchUpdateParam) => {
batchedUpdates.push(obj);
};
export function createStore(enhancer: (createStore: any) => Store) {
if (enhancer !== void 0) {
store = enhancer(createStore);
return store;
}
const componentUUIDToListeners: WeakMap<ReactionId, IdToListenersInfo> = new WeakMap();
function subscribe(listener: Function, uuid: ReactionId, idCustomType?: string) {
let isSubscribed = true;
const obj = componentUUIDToListeners.get(uuid);
if (obj === void 0 || obj.listeners === void 0) {
componentUUIDToListeners.set(uuid, {
idCustomType,
listeners: [listener],
});
} else if (!includes(obj.listeners, listener)) {
componentUUIDToListeners.set(uuid, {
idCustomType,
listeners: obj.listeners.concat(listener),
});
}
return function unsubscribe() {
if (!isSubscribed) {
return;
}
isSubscribed = false;
if (componentUUIDToListeners.has(uuid)) {
componentUUIDToListeners.delete(uuid);
}
};
}
let dirtyJob: Function | undefined;
let mutationDepth = 0;
let called = false;
const flushChange = (ids: ReactionId[], useExternalBatchUpdate: boolean, isInner: boolean) => {
if (!ids.length || ctx.disableReactive) {
return;
}
const pendingObjs: IdToListenersInfo[] = [];
for (let index = 0; index < ids.length; index++) {
const cid = ids[index];
const obj = componentUUIDToListeners.get(cid);
if (obj) {
pendingObjs.push(obj);
}
}
const flush = (idCustomType?: string) => () => {
for (let index = 0; index < pendingObjs.length; index++) {
const obj = pendingObjs[index];
if (obj.idCustomType === idCustomType) {
const listeners = obj.listeners;
for (let j = 0; j < listeners.length; j++) {
const listener = listeners[j];
listener(isInner);
}
}
}
};
if (useExternalBatchUpdate) {
batchedUpdates.forEach((obj) => {
const batchUpdate = obj.handler;
batchUpdate(flush(obj.idCustomType), () => {
// finish batchUpdate
ctx.batchUpdateOnFinish && ctx.batchUpdateOnFinish();
});
});
} else {
// reverse
flush()();
}
};
const keepAliveComputed = (isInner: boolean) => {
const computedReactionIds = [...triggerCollector.waitTriggerIds.values()]
.filter(id => id instanceof Reaction && !id.lazy && id.computed);
if (!computedReactionIds.length) {
return;
}
if (ctx.devTool) {
console.log('keepAlive computed', computedReactionIds);
}
flushChange(computedReactionIds, false, isInner);
// clean wait queue keep alive
batchRemoveFromSet(triggerCollector.waitTriggerIds, computedReactionIds);
};
const immediatelyReactive = (isInner: boolean) => {
const immediatelyReactionIds = [...triggerCollector.waitTriggerIds.values()]
.filter(id => id instanceof Reaction && !id.computed && id.immediately);
if (!immediatelyReactionIds.length) {
return;
}
// clean wait queue
batchRemoveFromSet(triggerCollector.waitTriggerIds, immediatelyReactionIds);
if (ctx.devTool) {
console.log('immediately reactive', immediatelyReactionIds);
}
flushChange(immediatelyReactionIds, false, isInner);
};
const nextTickQueue: Function[] = [];
const nextTickCaller = () => {
if (called) {
return;
}
called = true;
const func = () => {
nextTick(() => {
if (mutationDepth > 0) {
called = false;
nextTickQueue.shift();
return;
}
if (nextTickQueue.length === 0) {
return;
}
dirtyJob && dirtyJob();
});
};
nextTickQueue.push(func);
func();
};
const clean = (needClearHistory = true) => {
called = false;
dirtyJob = void 0;
nextTickQueue.shift();
actionTypeChain.length = 0;
needClearHistory && triggerCollector.endBatch();
};
function dispatch(dispatchedAction: DispatchedAction) {
const {
payload,
type,
original,
immediately,
isInner = false,
domain,
name,
forceSaveHistory = false,
isNeedRecord = true,
} = dispatchedAction;
if (isInner) {
TimeTravel.processing = true;
}
const syncStackId = materialCallStack.push({ type: type!, method: name, domain: domain?.constructor.name });
if (!isInner && (type !== EMaterialType.MUTATION && type !== EMaterialType.UPDATE)) {
return;
}
const callback = () => {
const ids = [...triggerCollector.waitTriggerIds.values()];
if (!ids.length && !forceSaveHistory) {
clean(false);
return;
}
if (!isInner && isNeedRecord) {
triggerCollector.save();
}
const computedReactionIds = ids.filter(id => id instanceof Reaction && id.lazy && id.computed);
// do lazy computed reaction first,PS:maybe add new trigger ids
if (ctx.devTool) {
console.log('lazy computed', computedReactionIds);
}
flushChange(computedReactionIds, false, isInner);
const lazyReactiveIds = [...triggerCollector.waitTriggerIds.values()].filter(id => id instanceof Reaction && !id.computed && !id.immediately);
const restIds = [...triggerCollector.waitTriggerIds.values()].filter(id => !(id instanceof Reaction));
clean();
// do lazy reactive
if (ctx.devTool) {
console.log('lazy reactive', lazyReactiveIds);
}
flushChange(lazyReactiveIds, false, isInner);
// do Reactive/ReactReactive/CustomReactive
if (ctx.devTool) {
console.log('custom view Reactive', restIds);
}
flushChange(restIds, true, isInner);
};
mutationDepth += 1;
if (isInner || (immediately && triggerCollector.waitTriggerIds.size > 0)) {
// flush previous job
dirtyJob && dirtyJob();
}
if (dirtyJob === void 0) {
dirtyJob = callback;
}
let result: any;
if (ctx.devTool) {
const event: BeforeStoreChangeEvent = {
domain: domain?.constructor.name ?? 'UNKNOWN',
method: name,
args: payload,
state: domain?.$$turboxProperties,
time: Date.now(),
stackId: materialCallStack.currentStack?.stackId
};
emitter.emit(EEventName.beforeStoreChange, event);
}
try {
result = (original as Mutation)(...payload);
} catch (error) {
return error;
}
if (ctx.devTool) {
const event: AfterStoreChangeEvent = {
domain: domain?.constructor.name ?? 'UNKNOWN',
method: name,
args: payload,
state: domain?.$$turboxProperties,
async: false,
time: Date.now(),
stackId: materialCallStack.currentStack?.stackId
};
emitter.emit(EEventName.afterStoreChange, event);
}
if (isPromise(result)) {
materialCallStack.pop(syncStackId);
return new Promise((resolve, reject) => {
let asyncStackId: number;
Promise.resolve().then(() => {
asyncStackId = materialCallStack.push({ type: type!, method: name, domain: domain?.constructor.name, syncStackId });
});
(result as Promise<void>).then((res) => {
if (ctx.devTool) {
emitter.emit(EEventName.asyncAfterStoreChange, {
domain: domain?.constructor.name ?? 'UNKNOWN',
method: name,
args: payload,
state: domain?.$$turboxProperties,
async: true,
time: Date.now(),
stackId: materialCallStack.currentStack?.stackId
});
}
keepAliveComputed(isInner);
immediatelyReactive(isInner);
mutationDepth -= 1;
if (immediately) {
// immediately execute
dirtyJob && dirtyJob();
} else {
nextTickCaller();
}
materialCallStack.pop(asyncStackId);
resolve(res);
}).catch((error) => {
reject(error);
});
});
}
keepAliveComputed(isInner);
immediatelyReactive(isInner);
mutationDepth -= 1;
if (isInner || immediately) {
// immediately execute
dirtyJob && dirtyJob();
TimeTravel.processing = false;
} else {
nextTickCaller();
}
materialCallStack.pop(syncStackId);
return result;
}
return {
dispatch,
subscribe,
};
}