看了 ssh 大佬的一篇文章,觉得挺有意思,刚好最近也在研究全局数据流方案,因此根据大佬的思路自己也尝试了一下。

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

这个包提供了几个核心 api :

effect

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

reactive

响应式机制核心 api ,将传入的对象转换为 proxy ,劫持上面所有属性的 getter 、setter 等方法,从而在访问数据的时候收集依赖(也就是 effect 函数),在修改数据的时候触发更新。

ref

reactive 函数可以将对象转换为响应式,不能转换基本类型,而 ref 函数可以转换基本类型,原理就是将基本类型用对象包装了一下,ref(0) 相当于 reactive({value: 0}) 。

computed

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

实现思路

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

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

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

因此思路就显而易见了:

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

store.ts 代码如下:

import * as React from 'react';
import { useEffection, useUpdate } from './shared';

// 全局 Context 对象
const StoreContext = React.createContext<any>(null);

export const useStore = <T, S>(selector: (store: T) => S): S => {
  // 强制组件渲染函数
  const forceUpdate = useUpdate();
  // 在组件中获取到全局上下文
  const store = React.useContext(StoreContext);
  // 使用 selector 访问 store 中的数据进行依赖收集
  // 修改数据的时候触发 scheduler 调用 forceUpdate 重新渲染组件
  const effection = useEffection(() => selector(store), {
    scheduler: job => {
      if (job() === undefined) return;
      forceUpdate();
    },
    lazy: true,
  });

  const value = effection();
  return value;
};

export const Provider = StoreContext.Provider;

再来看一下 useUpdate 和 useEffection 的实现:

import { effect, ReactiveEffect, stop } from '@vue/reactivity';
import { useCallback, useEffect, useRef, useState } from 'react';

// 强制渲染函数
// 使用了 ahooks 的实现方案
export const useUpdate = () => {
  const [, setState] = useState({});
  return useCallback(() => setState({}), []);
};

// 对 effect 方法的封装
export const useEffection = (...effectArgs: Parameters<typeof effect>) => {
  // 用一个ref存储effection
  // effect函数只需要初始化执行一遍
  const effectionRef = useRef<ReactiveEffect>();

  if (!effectionRef.current) {
    effectionRef.current = effect(...effectArgs);
  }

  // 卸载组件后取消effect
  const stopEffect = () => {
    stop(effectionRef.current!);
  };
  useEffect(() => stopEffect, []);

  // 上面的写法相当于
  // useEffect(() => {
  //   return () => {
  //     stop(effectionRef.current!);
  //   }
  // }, [])

  return effectionRef.current
};

使用方式

在 src 目录下建一个 store 目录,里面创建 index.ts 文件,内容如下:

import { computed, reactive } from '@vue/reactivity';

export type State = {
  count: number;
  name: string;
  age: number;
  users: string[];
  plusOne: number;
  fullName: string;
}

// 导出 state
export const state: State = reactive({
  count: 0,
  name: "dby",
  age: 12,
  users: ['dby', 'db', 'dm'],
  plusOne: computed<number>(() => state.count + 1),
  fullName: computed<string>(() => state.name + "dm")
});

// 导出 mutation
export const mutations = {
  add: () => state.count += 1,
  changeName: () => state.name = "dbydm",
  changeAge: () => state.age += 1,
  changeUser: () => state.users[1] = 'dbydm'
};

在 App.tsx 中,将 state 的内容传给 Provider :

import * as React from 'react';
import BaseInfo from './components/BaseInfo';
import { Provider } from './rxv/store';
import { state } from './store';

function App() {
	return (
		<Provider value={state}>
			<BaseInfo />
		</Provider>
	)
}

export default App;

然后在 BaseInfo 组件里面消费 state 的数据:

import * as React from 'react';
import { useStore } from '../rxv/store';
import { mutations, State } from '../store';

const BaseInfo: React.FC<{}> = () => {
	const state = useStore(({ name, age, fullName }: State) => {
    	return { name, age, fullName };
    })
    return (
		<>
	      <div>姓名:{state.name}</div>
	      <div>年龄:{state.age}</div>
	      <div>Name: {state.fullName}</div>
	      <div>
	        <button onClick={mutations.changeName}>Change Name</button>
	        <button onClick={mutations.changeAge}>Change Age</button>
	      </div>
	    </>
	)
}

export default React.memo(BaseInfo);

优点

直接引入 @vue/reacivity ,完全使用 Vue3 的 reactivity 能力,拥有 computed , effect 等各种能力,并且对于 Set 和 Map 也提供了响应式的能力。

之前也尝试过使用 useContext 和 useReducer 实现状态管理,但是 useReducer 是直接替换闭包中的值,导致一旦状态发生变更,Provider 下的所有组件都会重新渲染,哪怕没有消费 Context 下的任何信息。这种情况下,组件需要使用 React.memo 避免不必要的渲染(仅对消费了 Context 信息的组件进行渲染)。

而使用 rxv 可以实现组件级别的精确更新。例如 BaseInfo 和 Wrapper 是兄弟组件关系,都处于 Provider 的组件树下面,当 BaseInfo 消费的状态发生变化,如果 Wrapper 没有消费这些状态,即使没有用 React.memo 包裹也不会重新渲染。但如果 BaseInfo 和 Wrapper 是父子组件关系,BaseInfo 状态发生变化,Wrapper 不用 React.memo 包裹还是会重新渲染。

缺点

现在存在两个问题:

一个是 reactive 中定义 computed 之后,无法获取返回类型,只能手动声明。

另外一个是依赖收集全靠 useStore ,所以 selector 函数一定要精确的访问到你关心的数据,如果是数组类型,就必须访问数组的每一项进行依赖收集,否则使用数组变更方法或者通过下标修改数组不能触发组件重新渲染。

主要是第二个问题影响比较大,前后端交互的数据经常会涉及到嵌套结构,那么为了实现精确的依赖收集,就需要在 effect 函数里面把这些数据都访问一遍,例如 ssh 大佬给了一个方案:

function Logger() {
  const logs = useStore((store: Store) => {
    return store.state.logs.map((log, idx) => (
      <p className="log" key={idx}>
        {log}
      </p>
    ));
  });

  return (
    <Card hoverable>
      <h1>控制台</h1>
      <div className="logs">{logs}</div>
    </Card>
  );
}

原本列表渲染属于视图层逻辑,现在为了进行依赖收集,不得不放到 useStore 中进行了。个人认为其实也可以不用这种方式,只要在 effect 函数里面对嵌套的数据进行递归访问,也是可以实现依赖收集的。后期可以在这个基础上改进。

参考

React中引入Vue3的@vue/reactivity 实现响应式状态管理

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐