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 排版

相关推荐
计算机魔术师1 小时前
管谷歌的不是Pichai一个人,是一整层AI智能体
前端
米米商聊2 小时前
图标能点,按钮就可用了吗?键盘、名称与收藏状态
前端·javascript
Younglina2 小时前
三个晚上,我和 WorkBuddy 做了个生图网站
前端·后端·ai编程
xy34532 小时前
Axure 9.0 内联框架加载方式
前端·ui·html·axure·原型·产品设计
技术小甜甜3 小时前
【Cursor】Skill 被标黄色警告,怎么改成合格的 SKILL.md
开发语言·javascript·ai编程·cursor·1024程序员节·ai agent·skills
陈随易3 小时前
副业搞钱点子之收入查看器
前端·后端
Profile排查笔记3 小时前
浏览器指纹逆向怎么分析:从网络请求追踪到 JavaScript 采集链
前端·人工智能·后端·自动化
hudie7654 小时前
LikeShop 二开最常改的 5 个文件(附改前改后对比)
前端·uni-app·php·个人开发
qetfw4 小时前
Bash 函数与流程控制:read、if、for、case 验证
linux·前端·javascript·bash
鬼手点金5 小时前
从 API 消息到模型 Token:Chat Template
java·前端·javascript·数据库·人工智能·深度学习·1024程序员节