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

相关推荐
GDAL5 分钟前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿5 分钟前
react防止页面崩溃
前端·react.js·前端框架
z千鑫31 分钟前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js
m0_748256141 小时前
前端 MYTED单篇TED词汇学习功能优化
前端·学习
小马哥编程2 小时前
Function.prototype和Object.prototype 的区别
javascript
小白学前端6662 小时前
React Router 深入指南:从入门到进阶
前端·react.js·react
web130933203982 小时前
前端下载后端文件流,文件可以下载,但是打不开,显示“文件已损坏”的问题分析与解决方案
前端
王小王和他的小伙伴3 小时前
解决 vue3 中 echarts图表在el-dialog中显示问题
javascript·vue.js·echarts
学前端的小朱3 小时前
处理字体图标、js、html及其他资源
开发语言·javascript·webpack·html·打包工具
outstanding木槿3 小时前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架