自己动手实现状态管理
May 17, 2022
自己动手实现了一套前端状态管理,解决了 mobx 与 redux 的使用过程中的痛点,并设计了一套使用模式应用到了日常项目中。
前言
redux 和 mobx 不香吗,为什么要自己动手实现呢?只能说两个都香,但没那么香 😉。下面我浅谈一下这两个库在我使用过程中的一些痛点
Redux
- 副作用扔给中间件来处理,导致社区一堆中间件
- 太多样板式代码,一个简单的更新也要写很多代码
Mobx
- 更新数据没有约束,可以随意更改
实现
首先,作为一个前端状态管理库,必不可少的功能当然是响应式,这里采用 Rxjs 来实现。说到 Rxjs 可能大家都会想到 Observable
,但 Observable
本身是单播的,而且并不能给其提供新的值,也就是其值是不能被更改的,这样的话显然是满足不了我们的需求的;由此大家又可能会想到 BehaviorSubject
, BehaviorSubject
是可以很简单的通过 next(newVal)
去更新值,并且在提供新值之前的 subscriber 都会被通知到,但是,BehaviorSubject
是可以被随意 next
的,这和 mobx 有同样的问题。
我们可以反观 Observable
与 BehaviorSubject
,BehaviorSubject
可以让我们很简单的更新数据,Observable
的值又增加了约束,让我们不能更改其值,那我们可不可以让两者结合一下?
export class Store<T> extends Observable<T> {
public readonly store = this
private currentValue: T
protected _source: BehaviorSubject<T>
constructor(initialValue: T) {
super()
// 将 Observable 的 source 替换为 BehaviorSubject
this.source = this._source = new BehaviorSubject<T>(initialValue) this.currentValue = initialValue
}
get value() {
return this.currentValue
}
update(nextValue: T) {
this.emit(nextValue as T)
}
protected emit(value: T) {
this.currentValue = value
this._emit(value)
}
private _emit(value: T) {
this._source.next(value)
}
}
const a = new Store({ value: 4 })
a.subscribe((res) => {
console.info(res) // {value: 4}
})
a.update({ value: 5 }) // {value: 5}
以上就是这个状态管理库的全部,这里关键的思路就是将 Observable 的 source 替换成 BehaviorSubject,这样这个数据只能在内部通过 next 去更改,而对外其实输出的还是 Observable,使用者是不能随意更改的
a.update({ value: 5 }) // { value: 5 }
上面的更新方法会比较麻烦而且还有可能引起重渲染的问题,因此我们引入不可变数据库 immer 来解决
type Recipe<T> = (draft: T) => void
export class Store<T> extends Observable<T> {
public readonly store = this
private currentValue: T
protected _source: BehaviorSubject<T>
constructor(initialValue: T) {
super()
this.source = this._source = new BehaviorSubject<T>(initialValue)
this.currentValue = initialValue
}
get value() {
return this.currentValue
}
update(recipe: Recipe<T>, options?: { rollback?: boolean }) {
if (options?.rollback) {
const [nextValue, p, inversePatches] = produceWithPatches(
this.value,
recipe
)
this.emit(nextValue as T)
return () => {
this.applyPatches(inversePatches)
}
} else {
this.emit(produce(this.value, recipe))
}
}
// patch 更新,可用于推送更新数据
applyPatches(patches: Patch[]): void {
const nextValue = applyPatches(this.value, patches)
this.emit(nextValue)
}
protected emit(value: T) {
this.currentValue = value
this._emit(value)
}
private _emit(value: T) {
this._source.next(value)
}
}
const a = new Store({ value: 4, name: 'haoqin', age: 23, obj: { name: 'haoqin' } })
a.subscribe((res) => {
console.info(res)
})
a.pipe(
map(res => res.obj),
distinctUntilChanged()
).subscribe(console.info)
a.update((draft) => {
draft.value = 5
})
应用
数据层一共分为四层,首先介绍 Repo。Repo 是用于存放后端 api 的地方,在这里可以将多个 api 按功能统一掉,也可以统一后端 api 的返回,让 store 层拿到稳定的数据结构。
class Repo {
updateTask = async (id: TaskId, values: Partial<TaskSchema>) => {
const keys = Object.keys(values)
if (keys.includes('xxx')) {
return fetch('xxx', {
method: 'put'
})
}
}
}
Store 就是我们数据存储的地方,这里存放了我们所需要的各种数据,这里也是唯一能操作数据的地方,所以获取数据与操作数据的方法都会在这一层统一暴露给 Presenter
class Store {
protected repo = new Repo()
// 存储数据的地方
readonly root = createStore({ tasks: new Map() })
async updateTask(id: TaskId, values: Partial<TaskSchema>) {
const rollback = this.root.update(draft => {
const task = draft.tasks.get(id)
if (!task) return
for(const key of Object.keys(values)) {
// 乐观更新
task[key] = values[key]
}
}, { rollback: true })
try {
// 调用更新方法实际更新
this.repo.updateTask(id, values)
} catch(err) {
// 如果出错则撤回
rollback()
}
}
}
Presenter 这一层就会与视图直接沟通,一些灰度策略可能就会在这里处理掉。经过了 Repo 、Store、Presenter 的层层防护,暴露给视图的 api 也会更加的稳定。
class Presenter {
protected store = new Store()
getTask(id: TaskId) => {
return this.store.getTask()
}
}
因此我们 Page 就是视图层,每一个 Page 对应一套 Presenter 、Store、Repo。
// task-detail.context
export const TaskDetailContext = createContext()
export const useTaskDetailContext = useContext(TaskDetailContext)
export const TaskDetailContextProvider = (props) => {
const value = useMemo(() => {
return {
presenter: new Presenter()
}
}, [])
return (
<TaskDetailContext.Provider value={ value }>
{ props.children }
</TaskDetailContext.Provider>
)
}
// task-detail.page
const TaskDetailInner: FC = () => {
const { presenter } = useTaskDetailContext()
// ...
}
export const TaskDetail: FC = () => {
return (
<TaskDetailContextProvider>
<TaskDetailInner />
</TaskDetailContextProvider>
)
}
这样我们的 Page 就获得了 Presenter 中的所有数据与操作数据的方法,下面就可以愉快的编写我们的 UI 界面了