老奶奶直呼简单!60余行代码实现zustand

说到react中常用的状态管理工具,你可能会如数家珍,什么redux,mobx,saga,react-redux...

长期以来在react中应用最广泛的redux虽然能够完成各种基本功能,有庞大的插件系统的支持,但是常常会被人诟病使用非常繁琐,上手的心智负担太重,今天要介绍的zustand可以完美的解决这个问题。使用简洁丝滑,易于理解,你值得拥有。

基本使用

先来了解一下如何使用,打开zustand官网:

一个大大的猪四蛋坐在树桩上跟着鼠标来回晃动,一股非常高端气息扑面而来,zustand 在我心目中的逼格瞬间高大起来...

不错。不错。

首先使用npm安装一下:

js 复制代码
npm install zustand

根据官网中给出的例子,创建一个仓库文件store:

js 复制代码
import { create } from 'zustand'

const useStore = create((set) => ({
  bears: 0,
  increase: () => set((state) => ({ bears: state.bears + 1 })),
  remove: () => set({ bears: 0 }),
  update: (newBears) => set({ bears: newBears }),
}))

create​函数创建一个仓库,接收一个函数,其中默认传入set​函数,通过set​函数更新state​的值。

state​ 和修改 state​ 的方法都在create​中被定义。在set​中提供state​便于对值进行修改。

然后呢?

这就结束了。

使用的时候也很简单,在组件中引入store​的返回值,自定义函数获取想要的state​或者更新函数。

js 复制代码
import { useStore } from '../store'

// 获取state用于显示在页面中
function BearCounter() {
  const bears = useStore((state) => state.bears)
  return <h1>{bears} around here...</h1>
}

// 获取更新函数更新state
function Controls() {
  const increasePopulation = useStore((state) => state.increase)
  return <button onClick={increasePopulation}>one up</button>
}

zustand支持监听器,当数据变动时,执行收集的监听函数。

使用useStore​获取state​:

js 复制代码
useStore.subscribe((state) => {
    console.log(useStore.getState());
})

插件体系

zustand与redux一样,也支持插件体系。但是并不是zustand实现的功能,他只是把你传入的函数执行了一遍而已,同时会提供state​。

js 复制代码
const useStore = create((set) => ({}))

在创建store​时,会提供set、get、store 的三个参数,这样就可以使用zustand提供的这三个参数做一些额外的处理:

js 复制代码
function miniMiddleware(fn) {
  return function(set, get, store) {
	function todo(...args) {
      //...
	  // 你想做的事
    }
  
    return fn(todo, get, store)
 }
}

使用插件时创建store​可以包裹使用:

js 复制代码
const useStore = create(miniMiddleware((set) => ({})))

使用

基本概念了解了,接下来趁热打铁,用起来!用 zustand实现一个简单入门乞丐版的 todoList。

对了对比,先使用普通的 state 方式来实现:

js 复制代码
import { FC, useState } from 'react'
import { create } from 'zustand'

const List: FC = () => {

  function changeVal(e) {
    setVal(e.target.value)
  }

  const [val, setVal] = useState('')
  const [list, setList] = useState<Array<string>>([])

  function addItem() {
    setList([
      ...list,
      val
    ])
    setVal('')
  }

  function deleteItem(index: number) {
    let newList = list
    newList.splice(index, 1)
    setList([
      ...newList
    ])
  }

  return <div>
    <input type="text" value={val} onChange={(e) => changeVal(e)} />
    <button onClick={addItem}>++</button>
    <div>
      {
        list.map((c, i) => (
          <div key={i}>
            {c}
            <button onClick={() => deleteItem(i)}>delete</button>
          </div>
        ))
      }
    </div>
  </div>
}

export default List

接下来使用 zustand 改写,将列表放入到 store 中:

js 复制代码
import { FC, useState } from 'react'
import { create } from 'zustand'

const useStore = create((set) => ({
  list: [],
  add: (val) => {
    set(state => {
      return {
        list: [
          ...state.list,
          val
        ]
      }
    })
  },

  delete: (val) => {
    set(state => {
      return {
        list: state.list.filter(item => item !== val)
      }
    })
  },
}))


const List: FC = () => {

  function changeVal(e) {
    setVal(e.target.value)
  }

  const [val, setVal] = useState('')

  const list = useStore((state) => state.list)
  const add = useStore((state) => state.add)
  const deleteItem = useStore((state) => state.delete)

  function addItem() {
    add(val)
    setVal('')
  }

  return <div>
    <input type="text" value={val} onChange={(e) => changeVal(e)} />
    <button onClick={addItem}>++</button>
    <div>
      {
        list.map((c, i) => (
          <div key={i}>
            {c}
            <button onClick={() => deleteItem(c)}>delete</button>
          </div>
        ))
      }
    </div>
  </div>
}

export default List

大功告成!

实现

根据我们上面的用法可以看出来,create​函数主要是创建仓库,而返回值用来获取state​或者获取更新函数。

js 复制代码
const create = (createState) => {
	// 创建store并返回一系列的功能函数
    const api = createStore(createState)
	// selector就是开发者将来传入的获取state内部值的函数
    const useBoundStore = (selector) => useStore(api, selector)

    Object.assign(useBoundStore, api);

    return useBoundStore
}
  • createStore

replace​是 zustand 在 set 状态的时候默认是合并 state 操作,也可以传一个 true 改成替换。

总共分为以下几个步骤:

  1. 开发者传入的更新的参数是否为函数?

    1. 如果是函数则将state传入并执行函数,如果不是直接返回值
  2. 与上次更新的值是否一致?(判断多次更新值是否发生了变化,优化操作)

  3. 通过replace​对state进行不同的操作:合并/替换

  4. 还有一些其他功能:getState​返回 state

js 复制代码
const createStore = (createState) => {
    let state;
    // 更新
    const setState = (partial, replace) => {
	  // 是否为函数?
      const nextState = typeof partial === 'function' ? partial(state) : partial
	  // 值是否发生了变化?
      if (!Object.is(nextState, state)) {
        const previousState = state;
		// 对state进行何种操作?
        if(!replace) {
			// 合并
            state = (typeof nextState !== 'object' || nextState === null)
                ? nextState
                : Object.assign({}, state, nextState);
        } else {
			// 覆盖
            state = nextState;
        }
      }
    }
  
    const getState = () => state;
  
    const api = { setState, getState, destroy }
    // 初始化
    state = createState(setState, getState, api)

    return api
}

在此基础上实现subscribe​就很简单了,当调用subscribe​时手收集函数,state发生变动时,执行一遍。

同时提供一个销毁函数,用于将订阅函数进行手动销毁:

diff 复制代码
const createStore = (createState) => {
    let state;
++    const listeners = new Set();
    // 更新
    const setState = (partial, replace) => {
      const nextState = typeof partial === 'function' ? partial(state) : partial

      if (!Object.is(nextState, state)) {
        const previousState = state;

        if(!replace) {
            state = (typeof nextState !== 'object' || nextState === null)
                ? nextState
                : Object.assign({}, state, nextState);
        } else {
            state = nextState;
        }
++        listeners.forEach((listener) => listener(state, previousState));
      }
    }
  
    const getState = () => state;
  	  // 收集订阅函数
++    const subscribe= (listener) => {
++      listeners.add(listener)
++      return () => listeners.delete(listener)
++    }
  	  // 手动销毁
++    const destroy= () => {
++      listeners.clear()
++    }
  
    const api = { setState, getState, subscribe, destroy }
    // 初始化
    state = createState(setState, getState, api)

    return api
}

接下来就是需要执行开发者传入的函数,也就是这部分操作:

状态更改,触发渲染。可以使用useState​触发一个随机数的方式:

js 复制代码
function useStore(api, selector) {
	// 定义setState
    const [,forceRender ] = useState(0);
    useEffect(() => {
		// 使用订阅的方式,每次state变更都会触发重新渲染
        api.subscribe((state, prevState) => {
            const newObj = selector(state);
            const oldobj = selector(prevState);
			// 是否需要重新渲染?
            if(newObj !== oldobj) {
                forceRender(Math.random());
            }   
        })
    }, []);
    return selector(api.getState());
}

监听 state 的变化,变了之后,根据新旧 state​ 调用 selector​ 函数的结果,来判断是否需要重新渲染。

对于触发重新渲染的方式,react提供一个hook,专门用于处理这一类的事件:

useSyncExternalStore​是一个hook,用来定义外部 store​ ,store​ 变化以后会触发 重新渲染。

参数subscribe​:一个函数,接收一个单独的 callback​ 参数并把它订阅到 store​上。当 store​ 发生改变,它应当调用被提供的 callback​。这会导致组件重新渲染。subscribe​ 函数会返回清除订阅的函数。

通过useSyncExternalStore​可以很方便的实现当 store​ 中的state​改变,重新渲染组件。

具体使用方式

所以可以改写成这样:

js 复制代码
function useStore(api, selector) {

    function getState() {
        return selector(api.getState());
    }
  
    return useSyncExternalStore(api.subscribe, getState)
}

最后完整代码:

js 复制代码
import { useSyncExternalStore } from "react";

const createStore = (createState) => {
    let state;
    const listeners = new Set();
    // 更新
    const setState = (partial, replace) => {
      const nextState = typeof partial === 'function' ? partial(state) : partial

      if (!Object.is(nextState, state)) {
        const previousState = state;

        if(!replace) {
            state = (typeof nextState !== 'object' || nextState === null)
                ? nextState
                : Object.assign({}, state, nextState);
        } else {
            state = nextState;
        }
        listeners.forEach((listener) => listener(state, previousState));
      }
    }
  
    const getState = () => state;
  
    const subscribe= (listener) => {
      listeners.add(listener)
      return () => listeners.delete(listener)
    }
  
    const destroy= () => {
      listeners.clear()
    }
  
    const api = { setState, getState, subscribe, destroy }
    // 初始化
    state = createState(setState, getState, api)

    return api
}

function useStore(api, selector) {

    function getState() {
        return selector(api.getState());
    }
  
    return useSyncExternalStore(api.subscribe, getState)
}

export const create = (createState) => {
    const api = createStore(createState)

    const useBoundStore = (selector) => useStore(api, selector)

    Object.assign(useBoundStore, api);

    return useBoundStore
}

‍ The End。

‍‍ ‍写在最后

未来可能继续输出antd源码解析系列文章,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳 ‍ ‍ ‍ ‍

相关推荐
成都被卷死的程序员14 分钟前
响应式网页设计--html
前端·html
mon_star°33 分钟前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf219131845537 分钟前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
Mr_Xuhhh2 小时前
递归搜索与回溯算法
c语言·开发语言·c++·算法·github
文军的烹饪实验室2 小时前
ValueError: Circular reference detected
开发语言·前端·javascript
Martin -Tang3 小时前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发3 小时前
解锁微前端的优秀库
前端
王解4 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁4 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂4 小时前
工程化实战内功修炼测试题
前端·javascript