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

相关推荐
zqx_717 分钟前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己34 分钟前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称1 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色1 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
NiNg_1_2342 小时前
Vue3 Pinia持久化存储
开发语言·javascript·ecmascript
读心悦2 小时前
如何在 Axios 中封装事件中心EventEmitter
javascript·http
BigYe程普2 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
神之王楠2 小时前
如何通过js加载css和html
javascript·css·html