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

相关推荐
GISer_Jing4 分钟前
前端面试常考题目详解
前端·javascript
Boilermaker19921 小时前
【Java EE】SpringIoC
前端·数据库·spring
中微子1 小时前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上10241 小时前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y2 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁2 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry2 小时前
Fetch 笔记
前端·javascript
拾光拾趣录2 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟2 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan2 小时前
一文了解什么是Dart
前端·flutter·dart