React 的状态管理库,目前比较流行的是 Redux 和 Mobx。Recoil 是最近 Facebook 团队新发布的库,给人感觉是 React 官方推荐一样,就像 Vuex 对于 Vue,虽然 React 并没直接提及。
Motivation
在开展新的项目时,因为跟着公司的技术栈 React、Redux,原本考虑使用 Redux,不过 Redux 并不非常适用,在了解完 Recoil 的优点后决定使用 Recoil。
- Micro-Frontend 使得项目不需要多人协同,Redux 反而影响灵活
- Facebook 背书,不太担心维护和功能更新
- 渐进式和分布式,性能对比上也有优势
- 与 Concurrent 模式及其他 React 新特性兼容
概念
Recoil 的概念不多,思想上就是更细的颗粒度,主要理解 Atom
和 Selector
两个核心观念。而颗粒度的方向感觉是趋势,从最初要求 Components 拆分越来越细,到 State 越来越细,到现在的 SSR 也往细化的方向走,虽然不是说越细颗粒度就越好,但目前整个生态倒是都在往这方面走。
Atom
Atom 是状态单位。可更新和可订阅的:当一个原子更新时,每个订阅的组件都会用新值重新渲染。它需要唯一的 Key 用于调试、持久化和某些高级 API,有点像 React 的 Key 的概念。
const fontSizeState = atom({
key: 'fontSizeState',
default: 14,
})
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState)
return (
<button onClick={() => setFontSize(size => size + 1)} style={{ fontSize }}>
Click to Enlarge
</button>
)
}
function Text() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState)
return <p style={{ fontSize }}>This text will increase in size too.</p>
}
这样就可以做到跨组件的通信,共享 fontSize
状态。
因为每个 Atom 都需要唯一的 Key,当项目复杂后,Key 的管理也会比较麻烦,在这点上有点像回到 Redux
Selector
Selector 是纯函数,它接受 Atom 或其他 Selector 作为输入。当这些 Atom 或 Selector 更新时,Selector 也会更新。Components 可以像 Atom 一样订阅 Selector,然后在 Selector 更改时重新渲染。
Selector 用于计算基于状态的衍生状态。它就像 React 的 useEffect,依赖于它的输入,当依赖变化就会重新执行,并且可以多处结合使用。
const fontSizeLabelState = selector({
key: 'fontSizeLabelState',
get: ({ get }) => {
const fontSize = get(fontSizeState)
return `${fontSize}'px'`
},
})
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState)
const fontSizeLabel = useRecoilValue(fontSizeLabelState)
return (
<>
<div>Current font size: {fontSizeLabel}</div>
<button onClick={() => setFontSize(fontSize + 1)} style={{ fontSize }}>
Click to Enlarge
</button>
</>
)
}
Mini Recoil
我们都知道 Recoil 的核心就是 Atom 以及 Selector,带着了解 Recoil 的核心思想和逻辑,打算实现一个 Mini 的 Recoil,不需要太多功能和优化代码,主要是可以有最基础的功能。Mini Recoil,是实现的一个简单版的 Recoil。
Atom 是基础,Selector 也是在 Atom 的基础上去扩展。所以先实现一个可用的 Atom。
Recoil 采用的是订阅模式
Atom
定义一个 Atom 类,初始化 listeners
将 setter
和 getter
设置好,用于修改 state 的时候触发 render。
export const NameSpace = new Map()
type AtomContext = {
key: string
default: any
}
export class Atom<T> {
private listeners = new Set<(value: T) => void>()
constructor(private context: AtomContext) {
this.getter = this.getter.bind(this)
this.setter = this.setter.bind(this)
this.value = context.default
}
setter(value: T) {
// NOTE: Optimize stateValue === preStateValue
if (this.value === value) {
console.log('memo')
} else {
this.value = value
this.emit()
}
}
getter(): T {
return this.value
}
emit() {
for (const listener of this.listeners) {
const value = this.getter()
listener(value)
}
}
subscribe(callback: (value: T) => void): SubscribeReturn {
this.listeners.add(callback)
return {
unsubscribe: () => {
this.listeners.delete(callback)
},
}
}
}
// 在执行 atom 的时候对 key 做一下判断
export const atom = (value: AtomContext) => {
if (NameSpace.has(value.key)) {
throw new Error(`invalid key`)
} else {
const defaultValue = new Atom(value)
NameSpace.set(value.key, defaultValue)
return defaultValue
}
}
一个最基本的 Atom 就完成了,接下来需要处理的是 Hooks
Hooks
Recoil 的 Hooks 有很多,因为它具备非常多的功能,我们只需要最简单的 getState 和 setState 功能即可。
import { useState, useEffect } from 'react'
import { RecoilState } from './types'
const updateHooks = (recoilState: RecoilState) => {
const [, updateState] = useState({})
useEffect(() => {
const { unsubscribe } = recoilState.subscribe(() => updateState({}))
return () => unsubscribe()
}, [recoilState])
}
export const useRecoilValue = (recoilState: RecoilState) => {
updateHooks(recoilState)
return recoilState.getter()
}
export const useSetRecoilState = (recoilState: RecoilState) => {
return recoilState.setter
}
export const useRecoilState = (recoilState: RecoilState) => {
updateHooks(recoilState)
return [recoilState.getter(), recoilState.setter]
}
Recoil 的 update 逻辑,只有获取了 stateValue 才会将 update 加入到订阅,只使用 setState 是不会触发更新。
Selector
Selector 可以理解为 Atom 的扩展,两者可以继承一个 Basic 类,但是这里为了方便查看代码,直接 Copy 了一份。
import { NameSpace } from './atom'
import { RecoilState } from './types'
type SubscribeReturn = {
unsubscribe: VoidFunction
}
type SelectorGetGenerator<T> = (context: {
get: <V>(dep: RecoilState) => V
}) => T
type SelectorSetGenerator<T> = (context: {
get: <V>(dep: RecoilState) => V
set: <V>(dep: RecoilState, value?: any) => void
}) => T
type SelectConext = {
key: string
get: SelectorGetGenerator<any>
set: SelectorSetGenerator<any>
}
export class Selector<T> {
private listeners = new Set<(context: T) => void>()
private registeredDeps = new Set()
private bindAtom<V>(dep: RecoilState): V {
dep.subscribe(() => this.updateSelector())
this.registeredDeps.add(dep)
return dep.getter()
}
private updateSelector() {
this.value = this.context.get({ get: (dep: RecoilState) => dep.getter() })
this.emit()
}
constructor(private readonly context: SelectConext) {
this.getter = this.getter.bind(this)
this.setter = this.setter.bind(this)
this.value = context.get({ get: (dep: RecoilState) => this.bindAtom(dep) })
}
setter() {
this.context.set({
get: (dep: RecoilState) => dep.getter(),
set: (dep: RecoilState, value: any) => dep.setter(value),
})
}
getter(): T {
return this.value
}
emit() {
for (const listener of this.listeners) {
const value = this.getter()
listener(value)
}
}
subscribe(callback: (value: T) => void): SubscribeReturn {
this.listeners.add(callback)
return {
unsubscribe: () => {
this.listeners.delete(callback)
},
}
}
}
export const selector = (context: SelectConext) => {
if (NameSpace.has(context.key)) {
throw new Error(`invalid key`)
} else {
const defaultValue = new Selector(context)
NameSpace.set(context.key, defaultValue)
return defaultValue
}
}
可以看出来,整个 Selector 和 Atom 的区别不大,主要有以下几点
- stateValue,value 不能像 Atom 一样直接获取,因为
value = get()
,所以需要执行get
- 将
get
里面用到的 Atom 加上订阅并且需要registeredDeps
防止重复添加,用于 Atom 修改时将 Selector 更新 - 将
setter
和 Atom 进行绑定,不能像 Atom 一样直接修改当前 value,需要执行set
Conclusion
上面实现的 Mini Recoil 并不完善,例如一些 defaultValue 的 reset 功能,或者一些 async 功能都是没有的,当然还有一些性能的优化或者异常的处理都没有。以后有时间可能会尝试补全。
但这个 Mini Recoil 主要的就是了解 Recoil 的思路,它用订阅模式,将 Components 的 Update 放到 Atom 里面,并且可以使用 Selector 去扩展 Atom。
它让 Components 和 Atom(Selector) 挂钩,通过 Atom 的 setter
去 Emit 所有的 listeners,实现 Components 的 update,所以它不需要像 Redux 那样有比较复杂又规范的流程,至于性能方面我理解是两者的区别不会特别的大,Redux 在处理的好的情况下应该是和 Recoil 差不多的,但是 Recoil 因为已经细分到 Atom 对下限更有保证,而且思想上有区分颗粒度。在实现上我想应该用 Proxy 也是一样的。