在 React 中引入 Vue3 的 reactivity 分包来实现状态管理。

2020-01-26 18:32:06 +08:00
 sl1673495

前言

React 的状态管理是一个缤纷繁杂的大世界,光我知道的就不下数十种,其中有最出名 immutable 阵营的redux,有 mutable 阵营的mobxreact-easy-state,在 hooks 诞生后还有极简主义的unstated-next,有蚂蚁金服的大佬出品的hoxhoox

其实社区诞生这么多种状态管理框架,也说明状态管理库之间都有一些让人不满足的地方。

rxv是我依据这些痛点,并且直接引入了 Vue3 的 package: @vue/reactivity去做的一个 React 状态管理框架,下面先看一个简单的示例:

示例

// store.ts
import { reactive, computed, effect } from '@vue/reactivity';

export const state = reactive({
  count: 0,
});

const plusOne = computed(() => state.count + 1);

effect(() => {
  console.log('plusOne changed: ', plusOne);
});

const add = () => (state.count += 1);

export const mutations = {
  // mutation
  add,
};

export const store = {
  state,
  computed: {
    plusOne,
  },
};

export type Store = typeof store;
// Index.tsx
import { Provider, useStore } from 'rxv'
import { mutations, store, Store } from './store.ts'
function Count() {
  const countState = useStore((store: Store) => {
    const { state, computed } = store;
    const { count } = state;
    const { plusOne } = computed;

    return {
      count,
      plusOne,
    };
  });

  return (
    <Card hoverable style={{ marginBottom: 24 }}>
      <h1>计数器</h1>
      <div className="chunk">
        <div className="chunk">store 中的 count 现在是 {countState.count}</div>
        <div className="chunk">computed 值中的 plusOne 现在是 {countState.plusOne.value}</div>
         <Button onClick={mutations.add}>add</Button>
      </div>
    </Card>
  );
}

export default () => {
  return (
    <Provider value={store}>
       <Count />
    </Provider>
  );
};

可以看出,store的定义只用到了@vue/reactivity,而rxv只是在组件中做了一层桥接,连通了 Vue3 和 React,正如它名字的含义:React x Vue。

源码地址

https://github.com/sl1673495/react-composition-api

一些痛点

根据我自己的看法,我先简单的总结一下现有的状态管理库中或多或少存在的一些不足之处:

  1. redux为代表的,语法比较冗余,样板文件比较多。
  2. mobx很好,但是也需要单独的学一套 api,对于 react 组件的侵入性较强,装饰器语法不稳定。
  3. unstated-next是一个极简的框架,对于 React Hook 做了一层较浅的封装。
  4. react-easy-state引入了observe-util,这个库对于响应式的处理很接近 Vue3,我想要的了。

下面展开来讲:

options-based 的痛点

Vuex 和 dva 的options-based的模式现在看来弊端多多。具体的可以看尤大在vue-composition-api 文档中总结的。

简单来说就是一个组件有好几个功能点,但是这几个功能点在分散在data,methods,computed中,形成了一个杂乱无章的结构。

当你想维护一个功能,你不得不先完整的看完这个配置对象的全貌。

心惊胆战的去掉几行,改掉几行,说不定会遗留一些没用的代码,也或者隐藏在 computed 选项里的某个相关的函数悄悄的坑了你...

而 hook 带来的好处是更加灵活的代码组织方式。

redux

直接引入 dan 自己的吐槽吧,要学的概念太多,写一个简单的功能要在五个文件之间跳来跳去,好头疼。redux 的弊端在社区被讨论也不是一天两天了,相信写过 redux 的你也是深有同感。

unstated-next

unstated-next 其实很不错了,源码就 40 来行。最大程度的利用了 React Hook 的能力,写一个 model 就是写一个自定义 hook。但是极简也带来了一些问题:

  1. 模块之间没有相互访问的能力。
  2. Context 的性能问题,让你需要关注模块的划分。(具体可以看我这篇文章的性能章节
  3. 模块划分的问题,如果全放在一个 Provider,那么更新的粒度太大,所有用了 useContext 的组件都会重复渲染。如果放在多个 Provider 里,那么就会回到第一条痛点,这些模块之间是相互独立的,没法互相访问。
  4. hook 带来的一些心智负担的问题。React Hooks 你真的用对了吗?

react-easy-state

这个库引入的observe-util其实和 Vue3 reactivity 部分的核心实现很相似,关于原理解析也可以看我之前写的两篇文章:
带你彻底搞懂 Vue3 的 Proxy 响应式原理! TypeScript 从零实现基于 Proxy 的响应式库。
带你彻底搞懂 Vue3 的 Proxy 响应式原理!基于函数劫持实现 Map 和 Set 的响应式。

那其实转而一想,Vue3 reactivity 其实是observe-util的强化版,它拥有了更多的定制能力,如果我们能把这部分直接接入到状态管理库中,岂不是完全拥有了 Vue3 的响应式能力。

原理分析

vue-next是 Vue3 的源码仓库,Vue3 采用 lerna 做 package 的划分,而响应式能力@vue/reactivity被划分到了单独的一个 package 中

从这个包提供的几个核心 api 来分析:

effect

effect 其实是响应式库中一个通用的概念:观察函数,就像 Vue2 中的Watcher,mobx 中的autorunobserver一样,它的作用是收集依赖

它接受的是一个函数,这个函数内部对于响应式数据的访问都可以收集依赖,那么在响应式数据更新后,就会触发响应的更新事件。

reactive

响应式数据的核心 api,这个 api 返回的是一个proxy,对上面所有属性的访问都会被劫持,从而在 get 的时候收集依赖(也就是正在运行的effect),在 set 的时候触发更新。

ref

对于简单数据类型比如number,我们不可能像这样去做:

let data = reactive(2)
// 😭oops
data = 5

这是不符合响应式的拦截规则的,没有办法能拦截到data本身的改变,只能拦截到data身上的属性的改变,所以有了 ref。

const data = ref(2)
// 💕ok
data.value= 5

computed

计算属性,依赖值更新以后,它的值也会随之自动更新。其实 computed 内部也是一个 effect。

拥有在 computed 中观察另一个 computed 数据、effect 观察 computed 改变之类的高级特性。

实现

从这几个核心 api 来看,只要 effect 能接入到 React 系统中,那么其他的 api 都没什么问题,因为它们只是去收集 effect 的毅力,去通知 effect 触发更新。

effect 接受的是一个函数,而且 effect 还支持通过传入schedule参数来自定义依赖更新的时候需要触发什么函数,

rxv的核心 api: useStore接受的也是一个函数selector,它会让用户自己选择在组件中需要访问的数据。

那么思路就显而易见了:

  1. selector包装在 effect 中执行,去收集依赖。
  2. 指定依赖发生更新时,需要调用的函数是当前正在使用 useStore的这个组件的forceUpdate强制渲染函数。

这样不就实现了数据变化,组件自动更新吗?

简单的看一下核心实现

export const useStore = <T, S>(selector: Selector<T, S>): S => {
  const forceUpdate = useForceUpdate();
  const store = useStoreContext();

  const effection = useEffection(() => selector(store), {
    scheduler: forceUpdate,
    lazy: true,
  });

  const value = effection();
  return value;
};
  1. 先通过 useForceUpdate 在当前组件中注册一个强制更新的函数。
  2. 通过 useContext 读取用户从 Provider 中传入的 store。
  3. 再通过 Vue 的 effect 去帮我们执行 selector(store),并且指定 scheduler 为 forceUpdate,这样就完成了依赖收集。

就简单的几行代码,就实现了在 React 中使用@vue/reactivity中的所有能力。

优点:

  1. 直接引入 @vue/reacivity,完全使用 Vue3 的 reactivity 能力,拥有 computed, effect 等各种能力,并且对于 Set 和 Map 也提供了响应式的能力。后续也会随着这个库的更新变得更加完善的和强大。
  2. vue-next 仓库内部完整的测试用例。
  3. 完善的 TypeScript 类型支持。
  4. 完全复用 @vue/reacivity 实现超强的全局状态管理能力。
  5. 状态管理中组件级别的精确更新。
  6. Vue3 总是要学的嘛,提前学习防止失业!

缺点:

  1. 由于需要精确的收集依赖全靠useStore,所以selector函数一定要精确的访问到你关心的数据。甚至如果你需要触发数组内部某个值的更新,那你在 useStore 中就不能只返回这个对象本身。

源码地址

https://github.com/sl1673495/react-composition-api

如果你喜欢这个库,欢迎给出你的 star✨,你的支持就是我最大的动力~

2952 次点击
所在节点    程序员
2 条回复
meeken
2020-01-26 18:35:54 +08:00
马克
jianzhang810
2020-01-26 22:55:34 +08:00
码上

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/640409

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX