Bobolo
  • Home
  • Me

Recoil

浅析 Recoil

10 Min Read

Sun Apr 11 2021

Written By Bobolo

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 也往细化的方向走,虽然不是说越细颗粒度就越好,但目前整个生态倒是都在往这方面走。

recoil

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 类,初始化 listenerssettergetter 设置好,用于修改 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 也是一样的。

Reference

Powered by Bobolo

Copyright © Bobolo Blog 2021