React会这个API你就可以做出一个类似zustand的状态库

大家好,这里是梦兽编程,更多知识专栏关注梦兽编程梦兽编程。

相信2023年的今天大家都听闻过Zustand这一类的新型状态库,它可以完全脱离React传统状态库以来上下文进行通信。Zustand非常的轻量实现代码只有仅仅200行代码左右就可以实现。

只要你会fp(函数式编程规范中的观察值模型) + React 18 提供一个hooks就能完成这一项功能。

观察者模型

视频可以看观察者模式在函数式编程有多简单实现?

class派写法和fp派写法自己选一个就好了,概念都差不多的

zustand 中是如何实现的。

在vanilla.ts源码中,我们可以看到这么一段代码,这里我把必要的代码移除,让代码更加清晰。

ini 复制代码
// vanilla.ts
const createStoreImpl = createState => {
  let state;
  const listeners = new Set();

  const setState = (partial, replace) => {
     state = newState;
    listeners.forEach(listener => listener(state));
  };

  const getState = () => state;

  const subscribe = listener => {
    listeners.add(listener);
    return () => {
      listeners.delete(listener);
    };
  };

  const api = { setState, getState, subscribe };
  // 因为我们传进来的createState是一个 (setState,getState)=> ({})
  // 所以这里我们就可以subscribe给后面的React做铺垫
  state = createState(setState, getState, api);

  return api;
};

export const createStore = createState =>
  createState ? createStoreImpl(createState) : createStoreImpl;

如何使用

scss 复制代码
// 创建
const store = createStoreImpl({ count: 0 });
// 更新
store.setState({ count: 1 });
// 订阅
const unsubscribe = store.subscribe((state) => {
  console.log('State changed:', state);
});

store.setState({ count: 2 }); // 触发订阅的回调函数

unsubscribe(); // 取消订阅

store.setState({ count: 3 }); // 不会触发订阅的回调函数

store.destroy(); // 销毁这个store

它是如何更新React的?

我们都知道React是一个单向绑定的ui框架。它不像Ng2,vue2,修改值就能viewer就会更新这种mvvm。在react中希望更新viewer的操作交给开发者自己控制,这也是为什么在一个业务中你可以不用调试就可以猜到那里出问题(前提是你不用mbox)。

回想一下我们使用react,是不是经常需要这么做。

scss 复制代码
const [state,setState] = useState(0);

setState(1)

// renderer...

那问题来了,现在这种Zustand把状态丢给一个外部变量进行管理的状况库。是如何更新React的viewer?它没有Mbox这类可以在上下文中进行更新。多亏React18 带来的 新Api useSyncExternalStore,如果你使用React 18已经pr进去了,如果使用的16-18之间的版本。use-sync-external-store需要这个依赖包

我们想看一个简单的例子,看看官方是如何使用用的。

javascript 复制代码
// This is an example of a third-party store
// that you might need to integrate with React.

// If your app is fully built with React,
// we recommend using React state instead.

let nextId = 0;
let todos = [{ id: nextId++, text: 'Todo #1' }];
let listeners = [];

export const todosStore = {
  addTodo() {
    todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }]
    emitChange();
  },
  subscribe(listener) {
    listeners = [...listeners, listener];
    return () => {
      listeners = listeners.filter(l => l !== listener);
    };
  },
  getSnapshot() {
    return todos;
  }
};

function emitChange() {
  for (let listener of listeners) {
    listener();
  }
}


import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

export default function TodosApp() {
    // 最主要的是 todosStore subscribe 和 getSnapshot 的实现
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
  return (
    <>
      <button onClick={() => todosStore.addTodo()}>Add todo</button>
      <hr />
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
}
typescript 复制代码
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector'

const {useSyncExternalStoreWithSelector} = useSyncExternalStoreExports

const createImpl = (createState) => {
    // api 就是去获取一个 上面的 createStoreImpl 一个观察者对象
  const api = typeof createState === 'function'
    ? createStore(createState)
    : createState

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

  Object.assign(useBoundStore, api)

  return useBoundStore
}

export const create = (createState) => createState
    ? createImpl(createState)
    : createImpl


export function useStore<TState, StateSlice>(
  api: WithReact<StoreApi<TState>>,
  selector: (state: TState) => StateSlice = api.getState as any,
  equalityFn?: (a: StateSlice, b: StateSlice) => boolean
) {
    // 想想上面的例子 所以我们的在 set的时候就能通知到React需要去做Render Viewer了
  const slice = useSyncExternalStoreWithSelector(
    api.subscribe,
    api.getState,
    api.getServerState || api.getState,
    selector,
    equalityFn
  )
  useDebugValue(slice)
  return slice
}

为什么React18需要提供这么一个API?

为了解决并发模型下tearing的问题,还不知道什么是tearing,可以谷歌一下。这个概念在国外的程序员已经讨论很久了。

并发渲染是很棒的,但是对于依赖于外部存储的库来说,可能会出现 tearing 问题。 tearing 是指用户可以看到的视觉不一致,即 UI 对于相同的状态显示多个值。通过比较同步渲染和并发渲染的过程,我们可以了解 tearing 在并发渲染中发生的区别。

useSyncExternalStore是React 18中为解决这个问题而引入的新钩子。该钩子基本上接收两个函数作为参数。

结语

看完这个文章,你也可以写出一个轻量级的状态库。嘻嘻是不是很开心呢?这里是梦兽编程期待在下一篇文章中再次见到你!感谢你的阅读。

截屏2023-08-17 23.44.00.png

我的B站视频号更多视频动态。

截屏2023-08-18 00.02.24.png

本文使用 markdown.com.cn 排版

相关推荐
web打印社区5 分钟前
C-Lodop 提示未准备好或 WebSocket 没准备好:先让本机服务起来
javascript·网络·websocket·网络协议·pdf·html
言乐66 分钟前
JavaScript概括前端原理
开发语言·前端·javascript·python·ecmascript
回眸&啤酒鸭33 分钟前
DeepSeek V4.1 Flash 批量处理实战指南
服务器·前端·javascript
用户33740969242633 分钟前
2026-09-27-Qwen-Image-2.1-1660Ti本地部署实战
前端
BIM云平台开发35 分钟前
DAZ里,如何保存现在姿势,如果调错了,可以重新导回
linux·服务器·前端·daz基础教程
传人37 分钟前
网页文字滚动效果如何做
前端·javascript·jquery
福兮说40 分钟前
前后端算的 MD5、SHA-256 对不上?编码、换行、BOM、HMAC、JSON 顺序,八个原因逐个实测
前端·javascript·node.js·json·哈希算法
一木 之林41 分钟前
DeepSeek Agent 开发
java·前端·人工智能
王霸天1 小时前
Three.js 模型体积优化:Draco/Meshopt 压缩与 DRACOLoader 配置的 4 个步骤
java·前端·javascript
AAA程序技术1 小时前
用户点击按钮时,发生了什么
前端