This site runs best with JavaScript enabled.

💾 数据管理层 NOVUS



集中管理数据状态模型,并且享受 TypeScript 的便利,从明天起,做一个幸福的人,不去关心同步异步、不去关心一个逻辑写多处、不去关心复杂的心智模型、不去关心不可推断的运行时错误。从今以后简单点,只关心数据和变更,只使用代码提示,就此一生,完成项目。
点击按钮尝试 demo:



代码

源码:
NOVUS

📦 实现一个简单朴素的 react 数据管理层


使用范例:
novus.tsx
"use client";
// novus.tsx
import React, {
Component,
ComponentClass,
FunctionComponent,
useState,
useEffect,
useRef,
} from "react";
interface TModelsPool {
[ind: string]: NovusBaseModel<any, any>;
}
type IDisposable = () => void;
type TElement<T> = string | ComponentClass<T, any> | FunctionComponent<T>;
export type TConnectProps = {
getModel: <K extends keyof TModelsPool>(name: K) => TModelsPool[K];
getModels: () => TModelsPool;
};
// simple polyfill for requestIdleCallback
const _requestIdleCallback =
(window || (global as any)).requestIdleCallback ||
((cb: Function) => setTimeout(cb, 1));
const _cancelIdleCallback =
(window || (global as any)).cancelIdleCallback ||
((id: number) => clearTimeout(id));
let subscribeIndex = 0;
let idleHandler: number;
class Novus {
// 提供一个集中的聚合地方,这块可以不使用
models: TModelsPool = {};
// 用订阅通知的模式,简单实现数据变更通知,外部可以不关心
listeners: Map<string, [Function, string[]]> = new Map(); // private
/**
* 绑定数据模型,集中管理数据模型
*
* @param model 数据模型 NovusBaseModel
*/
bindModel(model: NovusBaseModel<any>): IDisposable {
if (this.models[model.namespace]) {
console.error("sorry, you should bind with unque namespace!!");
delete this.models[model.namespace];
}
model.novus = this;
this.models[model.namespace] = model;
return () => {
delete this.models[model.namespace];
};
}
/**
* 订阅器,订阅对应 model 改变的函数,外部可以不关心
*
* @param func 订阅触发函数
* @param deps 订阅的 model,仅订阅的 model 改变后才会触发函数
*
* @returns 返回取消订阅的函数
*/
subscribe = (func: Function, deps: string[] = []): IDisposable => {
if (deps.length == 0 || typeof deps === "undefined")
console.warn(
"you didn't specify the dependency list, it works, but we recommend you fill a list to get a better performance"
);
const id = (subscribeIndex++).toString();
this.listeners.set(id, [func, deps]);
return () => {
this.listeners.delete(id);
};
};
/**
* 触发器,更新完数据仓之后必须触发才能通知 UI 变更
*
* @param namespace model 的命名空间 id,只会触发订阅对应 model 的 mapStatetoProps 函数。留空表示触发全部
*/
notifyChangesListeners = (namespace?: string) => {
this.listeners.forEach((v, key) => {
if (
typeof namespace === "undefined" ||
v[1].length === 0 ||
v[1].includes(namespace)
) {
// 将需要通知的函数放进队列等待通知
this.needNotifiedListeners.add(key);
}
});
// 需要通知各个订阅了
this.startNotifies();
};
needNotifiedListeners = new Set<string>([]);
startNotiTime: number;
startNotifies = () => {
if (idleHandler) _cancelIdleCallback(idleHandler);
// 执行之前先标记下时间
this.startNotiTime = new Date().getTime();
this.doTheNotifies();
};
/**
* 确实去通知订阅的部分,根据不同的执行可能再不同的宏任务队列被执行
*/
doTheNotifies = () => {
if (this.needNotifiedListeners.size > 0) {
const nextId = this.needNotifiedListeners.values().next().value;
this.needNotifiedListeners.delete(nextId);
this.listeners.get(nextId)[0]();
// 如果当前渲染的递归还有时间,继续,否则等待下一次宏任务
if (new Date().getTime() - this.startNotiTime < 16) {
this.doTheNotifies();
} else {
idleHandler = _requestIdleCallback(this.startNotifies);
}
}
};
connectProps = {
getModel: <K extends keyof TModelsPool>(name: K): TModelsPool[K] => {
return this.models[name];
},
getModels: (): TModelsPool => this.models,
};
/**
* 将 models state 数据转换挂载到组件上,类似 react-redux 的 connect 功能,不推荐,但是支持
*
* @param Comp 要挂载的组件,props 需求有基础的和被 connected 的
* @param mapStatetoProps 数据转换函数
* @param deps 依赖的 models,不在这个数组的 models 改变不会执行 mapStatetoProps 函数
*
* @returns NewComponent 返回新的组件,props 提示只有基础的
*/
connect = <OriginProps, ConnectedProps>(
Comp: TElement<OriginProps & ConnectedProps & TConnectProps>,
mapStatetoProps: (
models: TModelsPool,
app: Novus,
props: OriginProps
) => ConnectedProps,
deps: string[] = []
): React.ComponentClass<OriginProps> => {
class OutputCom extends Component<OriginProps, ConnectedProps> {
constructor(props: OriginProps) {
super(props);
this.state = mapStatetoProps(novus.models, novus, props);
}
dispose: Function;
componentDidMount() {
this.dispose = novus.subscribe(() => {
let newState = mapStatetoProps(novus.models, novus, this.props);
this.setState(newState);
}, deps);
}
componentWillUnmount() {
if (typeof this.dispose !== "undefined") {
this.dispose();
}
}
render() {
const allProps = {
...this.props,
...this.state,
...novus.connectProps,
};
return <Comp {...allProps} />;
}
}
return OutputCom;
};
/**
* 删除全部模型
*/
clean = () => {
for (let m in this.models) delete this.models[m];
this.listeners.clear();
this.needNotifiedListeners.clear();
};
}
export const novus = new Novus();
/**
* 一个基础的数据模型。用户可以继承此类后自己添加更新函数
*
* - namespace 作为唯一的 id
* - state 作为数据储存仓
* - setState 同步更新 state 的函数
* - actions 用户的数据模型的更新函数,支持异步函数
*/
export class NovusBaseModel<S, A = {}> {
/**
* 唯一的命名空间
*/
namespace: string;
novus: Novus = novus;
/**
* 数据仓
*
* 由于继承后会 state 覆盖,所以子组件要显式指定类型
*/
state: Readonly<S>;
/**
* 纯函数的数据更新函数,同步执行
*
* @param state 要更新的属性或者函数
*/
protected setState = <K extends keyof S>(
state: Pick<S, K> | ((states: S) => Pick<S, K>)
) => {
if (typeof state === "function") {
const newState = state(this.state);
this.state = { ...this.state, ...newState };
} else {
this.state = { ...this.state, ...state };
}
this.notifyChanges();
};
protected getModel = <K extends keyof TModelsPool>(
modelName: K
): TModelsPool[K] => {
return novus.models[modelName];
};
/**
* 通过标志变量控制多次的事件通知在一个 EventLoop 中只触发一次
*/
protected waitingNotify = false;
/**
* 通知上层触发订阅的 connect 转换函数,每次修改 state 必须触发!!!
*/
protected notifyChanges = () => {
// setState 是同步的,但是事件通知不是同步
if (this.waitingNotify) return;
this.waitingNotify = true;
new Promise((r) => r()).then(() => {
novus.notifyChangesListeners(this.namespace);
this.waitingNotify = false;
});
};
/**
* 数据更新的函数,相当于 effects 或者 reducers 的概念集合
*/
readonly actions: A;
}
/**
* 提供 hook 函数,在函数组件中非常简洁的使用
*
* @template T
* @param {(models: TModelsPool, app: Novus) => T} mapStatetoHooks 怎么从 models 中拿到状态
* @param {string[]} [deps] 依赖哪些 models 的变更通知
* @returns {T}
*/
export const useNovus = function <T>(
mapStatetoHooks: (models: TModelsPool, app: Novus) => T,
deps?: string[]
): T {
// don't run deps collect after first render
const initState = useRef<T | null>(null);
let _deps = deps;
if (initState.current === null) {
if (typeof deps === "undefined") {
const autoDeps = new Set([]);
const handler = {
get(target: TModelsPool, key: string) {
if (typeof target[key] !== "undefined") {
autoDeps.add(key);
}
return target[key];
},
};
const autoModels = new Proxy(novus.models, handler);
initState.current = mapStatetoHooks(autoModels, novus);
_deps = Array.from(autoDeps);
} else {
initState.current = mapStatetoHooks(novus.models, novus);
}
}
let [state, setState] = useState<T>(initState.current);
useEffect(() => {
const dispose = novus.subscribe(() => {
setState((oldState) => {
let newState = mapStatetoHooks(novus.models, novus);
// ↓ 浅比较一下判断是否需要更新 state,触发订阅
for (const key in oldState) {
if (oldState[key] !== newState[key]) {
return newState;
}
}
return oldState;
});
}, _deps);
return () => {
dispose();
};
}, []);
return state;
};
/**
* 封装一个组件,默认携带 novus 属性,方便引用
*
* @export
* @class NovusComponent
* @extends {(Component<P, S, SS>)}
* @template P
* @template S
* @template SS
*/
export class NovusComponent<P, S = {}, SS = any> extends Component<P, S, SS> {
novus = novus;
}
export default Novus;
Copy
// todoModel.ts
import {NovusBaseModel, novus} from './novus'
const todoDto = {
sync: async newTodo => {
await new Promise(r => setTimeout(r, 1000))
},
}
export enum TodoStatus {
Normal,
Completed,
Deleted,
}
export type TTodo = {
status: TodoStatus
tags: string[]
content: string
}
export interface ITodoState {
loading: boolean
groups: string[]
list: TTodo[]
filters: {
tags: string[]
statuses: TodoStatus[]
}
}
// 使用继承类的形式限制属性和方法
class TodoModel extends NovusBaseModel<ITodoState> {
namespace = 'todoModel'
constructor() {
super()
this.state = {
loading: false,
groups: [],
list: [],
filters: {
tags: [],
statuses: [],
},
}
}
// 自定义状态更新函数
syncData = async list => {
this.setState({loading: true})
await todoDto.sync(list)
this.setState({list, loading: false})
}
// actions 集合,可以调用异步函数,约定为修改状态的逻辑,提供数据操作方法
actions = {
create: async (content: string, tags: string[] = []) => {
const newTodo = {
tags,
content,
status: TodoStatus.Normal,
}
await this.syncData([newTodo, ...this.state.list])
},
update: async (index: number, todo: TTodo) => {
const newTodo = {
...this.state.list[index],
...todo,
}
await this.syncData([
...this.state.list.slice(0, index - 1),
newTodo,
...this.state.list.slice(index + 1),
])
},
delete: async (index: number) => {
await this.syncData([...this.state.list.slice(0, index), ...this.state.list.slice(index + 1)])
},
}
}
export const model = new TodoModel()
novus.bindModel(model)
Copy
import React, {useCallback} from 'react'
import {useNovus} from './novus'
import {model as todoModel, TTodo} from './todoModel'
const ExampleTodoExample = props => {
let [list, loading] = useNovus<[TTodo[], boolean]>(
models => {
return [models.todoModel.state.list, models.todoModel.state.loading]
},
['todoModel'],
)
const deleter = useCallback(async (index: number) => {
await todoModel.actions.delete(index)
}, [])
const adder = useCallback(async () => {
await todoModel.actions.create(new Date().toTimeString())
}, [])
return (
<div>
<button disabled={loading} onClick={adder}>
{loading ? 'loading' : 'ADD'}
</button>
<table>
<tbody>
{list.map((todo, index) => {
return (
<tr key={todo.content}>
<td>{todo.content}</td>
<td>
<button onClick={deleter.bind(null, index)}>关闭</button>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}
export default ExampleTodoExample;
Copy

🐞 关于

有时间就会分享一些技术文章、专业内容、经典问题、系列功能等。

{⛔ 未标注内容均为原创,请勿转载 ©️}