简单实现 Recoil 的状态订阅共享

时间:2021-2-20 作者:admin

Recoil 是一个新的 React 状态管理库,现在还处于试验阶段,它提出了分散式的原子化状态管理,提供 Hooks 式的 API 用于设置和获取状态,并使组件订阅状态。本文简单的实现了 Recoil 中使多个组件共享并订阅某个 state 的原理。

关于怎么使用 recoil 和 recoil 的原理, 我写了一篇文章,可以前往 这里

直接上代码:

// my-recoil
import React,{ useEffect, useState, useRef, useContext } from 'react';

const nodes = new Map()
const subNodes = new Map()

class Node{
  constructor(k, v){
    this.key = k
    this.value = v
  }

  getValue(){
    return this.value 
  }

  setValue(newV) {
    this.value = newV
  }
}

export function useMySetRecoilState(atom) {
  const { key, defaultValue } = atom
  let node
  const store = useStoreRef().current
  const hasNode = store.atomValues.has(key)
  if (hasNode) {
    node = store.atomValues.get(key)
  } else {
    const newNode = new Node(key, defaultValue)
    store.atomValues.set(key, newNode)
    node = store.atomValues.get(key)
  }

  const setState = (newValueOrUpdater) => {
    let newValue
    if (typeof newValueOrUpdater === 'function') {
      newValue = newValueOrUpdater(node.getValue())
    }
    node.setValue(newValue)
    store.atomValues.set(key, node)
    store.replaceState()
  }
  return setState
}

let subID = 0
function subRecoilState(store, atomkey, subid, cb) {
  if(!store.nodeToComponentSubscriptions.has(`${subid}-${atomkey}`)){
    store.nodeToComponentSubscriptions.set(`${subid}-${atomkey}`, cb)
  }
}
export function useMyRecoilValue(atom) {
  const [_, forceUpdate] = useState([])

  const { key, defaultValue } = atom
  const storeRef = useStoreRef()
  const store = storeRef.current

  let hasNode = store.atomValues.has(key)
  let node
  if (!hasNode) {
    node = new Node(key, defaultValue)
    store.atomValues.set(key, node)
  }
  node = store.atomValues.get(key)

  useEffect(() => {
    subRecoilState(store, key, subID++, () =>{
      forceUpdate([])
    })
  }, [key, node, store, storeRef])

  return node.getValue()
}

export function useMyRecoilState(atom) {
  return [useMyRecoilValue(atom), useMySetRecoilState(atom)]
}

const storeContext = React.createContext()

export const useStoreRef = () => useContext(storeContext)

export default function MyRecoilRoot({children}) {
  const notifyUpdate = useRef()


  function setNotify(x) {
    notifyUpdate.current = x
  }
  function Batcher({setNotify}) {
    const [_, setstate] = useState([])
    setNotify(() => setstate({}))

    useEffect(() => {
      // 广播更新事件
      storeState.current.nodeToComponentSubscriptions.forEach((cb) => {
        cb()
      })
    })

    return null
  }
  function replaceState() {
    notifyUpdate.current()
  }
  const storeState = useRef({
    atomValues: nodes,
    replaceState,
    nodeToComponentSubscriptions: subNodes
  })

  return <div>
    <storeContext.Provider value={storeState}>
      <Batcher setNotify={setNotify}/>
      {children}
    </storeContext.Provider>
  </div>
}

然后我们来使用这个 my-recoil 库,在另一个文件里定义三个 React 组件,并使用 my-recoil 提供的 MyRecoilRootuseMyRecoilStateuseMyRecoilValue,分别模拟 Recoil 的 RecoilRootuseRecoilStateuseRecoilValue

import React from 'react'
import MyRecoilRoot, {useMyRecoilState, useMyRecoilValue} from './recoil'

const countAtom = {
  key: 'count_atom',
  defaultValue: 0
}


const style = {border: 'solid 1px #456', width: '200px', margin: '20px'}
function Com1() {
  const [count, setCount] = useMyRecoilState(countAtom)

  function handleChange(){
    setCount(count => count + 1)
  }
  return (
    <div style={style}>
     <h2>组件1</h2>
     <div>count: {count}</div>
      <button onClick={handleChange}>点击更新组件1,看组件2、3会否更新</button>
    </div>
  )
}

function Com2() {
  const count = useMyRecoilValue(countAtom)
  return (
    <div style={style}>
      <h2>组件2</h2>
      <div>count: {count}</div>
    </div>
  )
}

function Com3() {
  const count = useMyRecoilValue(countAtom)
  return (
    <div style={style}>
      <h2>组件3</h2>
      <div>count: {count}</div>
    </div>
  )
}

App.whyDidYouRender = true
export default function App() {

  return <MyRecoilRoot>
    <Com1/>
    <Com2/>
    <Com3/>
  </MyRecoilRoot>
}

组件 Com1 ,Com2,Com3 将会订阅 countAtom,当在 Com1 中改变 countAtom 的值,Com2 和 Com3 会收到变化通知,更新组件。如下我们点击组件 Com1 的按钮,更新 countAtom 的值,组件 Com2 、Com3 也会收到通知触发 re-render:

现在我们再来解释 my-recoil 的原理。

首先,定义一个 MyRecoil 根组件,使用 context 把子组件包起来,在这里我们把 store 定义在 context 上,并且定一个 useStoreRef :

export const useStoreRef = () => useContext(storeContext)

这样子组件就可以使用 useStoreRef 来获取 store 了。每当我们在一个组件里面使用 useMyRecoilValue(someAtom) ,就会使用 useState 来定一个空的 state, 并返回一个 forceUpdate,只要调用 forceUpdate,就会触发更新,重新获取 store 中的 someAtom 的值, 触发更新的逻辑由 subRecoilState 来定义,subRecoilState 会在 store 上定一个 nodeToComponentSubscriptions,把每次调用 useMyRecoilValue(someAtom)时生成的 forceUpdate 放在 nodeToComponentSubscriptions 上面,等到调用 useMySetRecoilState(someAtom) 来设置 someAtom 的值的时候,就会调用 Batcher 的 setState([]),Batcher 被触发更新,于是里面的 useEffect 会执行下面这段代码:

useEffect(() => {
      // 广播更新事件
      storeState.current.nodeToComponentSubscriptions.forEach((cb) => {
        cb()
      })
    })

把 nodeToComponentSubscriptions 中的 forceUpdate 取出来执行,也就是触发 useMyRecoilValue(someAtom) 更新,获取新的 state,从而触发组件更新。这样就实现了组件订阅 store state。

这就是 Recoil 中实现订阅和共享状态的大致逻辑。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。