自己动手实现状态管理

May 17, 2022

自己动手实现了一套前端状态管理,解决了 mobx 与 redux 的使用过程中的痛点,并设计了一套使用模式应用到了日常项目中。

前言

redux 和 mobx 不香吗,为什么要自己动手实现呢?只能说两个都香,但没那么香 😉。下面我浅谈一下这两个库在我使用过程中的一些痛点

Redux

  1. 副作用扔给中间件来处理,导致社区一堆中间件
  2. 太多样板式代码,一个简单的更新也要写很多代码

Mobx

  1. 更新数据没有约束,可以随意更改

实现

首先,作为一个前端状态管理库,必不可少的功能当然是响应式,这里采用 Rxjs 来实现。说到 Rxjs 可能大家都会想到 Observable,但 Observable 本身是单播的,而且并不能给其提供新的值,也就是其值是不能被更改的,这样的话显然是满足不了我们的需求的;由此大家又可能会想到 BehaviorSubjectBehaviorSubject是可以很简单的通过 next(newVal)去更新值,并且在提供新值之前的 subscriber 都会被通知到,但是,BehaviorSubject是可以被随意 next,这和 mobx 有同样的问题。

我们可以反观 ObservableBehaviorSubjectBehaviorSubject可以让我们很简单的更新数据,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
})

应用

Store@2x.png

数据层一共分为四层,首先介绍 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 界面了