如何在 React 中使用 SolidJS 的 Signal?

Signal 的概念在社区已经讨论了一段时间了,作为细粒度的响应式概念,Signal 给前端领域带来了不少新内容,如 SolidJS、Preact Signals 等等。而翻看 SolidJS 和 Preact Signals 的源码会发现,在 Signal 的实现上两者并没有太大差异,甚至于和早期用 JavaScript 实现 Signal 的 S.js 也没有太大差异。Signal 的实现如此稳定的原因在于,Signal 本身的概念非常成熟且稳定。

对 Signal 本身和 SolidJS 感兴趣的同学,可以移步SolidJS 是如何实现响应式的? ,对 Signal 和 SolidJS 的响应式实现做了简单的介绍。

思路

动手之前建议先了解 SolidJS 的 createSignal,对 SolidJS 不是特别了解的同学建议先移步 SolidJS 官方文档:

docs.solidjs.com/

对 SolidJS 的 createSignal 的使用方式足够了解了之后,就可以开始动手了。

首先明确在 React 中使用 SolidJS 的 Signal 应该是一个什么样的形式:

tsx 复制代码
import useSignal from './useSignal';

function App() {
  const [count, setCount] = useSignal();

  return <button onClick={() => setCount(count() + 1)}>
    count is {count()}
  </button>
}

最终的使用方式应该与 SolidJS 中 createSignal 的使用方式相似,返回 gettersetter,调用 setter 修改 Signal 的值时触发视图更新。

有了这个思路就可以开始动手了。

创建一个名称为 useSignal 的 Hook,直接将 gettersetter 返回:

tsx 复制代码
import { createSignal } from 'solid-js';

export default function useSignal<T>(value: T) {
  const [getter, setter] = createSignal(value);

  return [getter, setter];
}

回到 App 组件中,点击 button 会发现页面上的 count 并没有更新,是因为 SolidJS 的 Signal 并不能直接触发 React 组件重新渲染。

这个时候就需要想一个办法让组件重新渲染,最常用的办法就是使用 useState 创建一个 State,通过更新 State 触发组件重新渲染。

所以需要将 setter 包一层,在调用 setter 更新 Signal 之后更新 State 触发组件重新渲染:

tsx 复制代码
import { useState } from 'react';
import { createSignal } from 'solid-js';

export default function useSignal<T>(value: T) {
  const [, update] = useState(0);
  const [getter, setter] = createSignal(value);

  return [
    getter,
    (value: T) => {
      setter(value);
      update((ticket) => ticket + 1);
    },
  ];
}

再次回到 App 组件中,点击 button 会发现页面上的 count 依旧没有更新。

为什么呢?很奇怪对不对?是 State 更新了之后没有触发组件重新渲染吗?还是调用 setter 之后根本没有更新 getter 的返回值?可能的原因无非就这两个,接下来一步一步排查。

使用 SolidJS 的 createEffect 在控制台输出一下 getter 的返回值看看,调用 setter 之后是否更新了 getter 的返回值:

tsx 复制代码
import { useState } from 'react';
import { createSignal, createEffect } from 'solid-js';

export default function useSignal<T>(value: T) {
  const [, update] = useState(0);
  const [getter, setter] = createSignal(value);

  createEffect(() => {
    console.log(getter());
  });

  return [
    getter,
    (value: T) => {
      setter(value);
      update((ticket) => ticket + 1);
    },
  ];
}

再次回到 App 组件中,点击 button 会发现控制台先打印出了 1,然后又快速打印出了 0,同时还伴随着一条黄色的 Warning。

先不管 Warning 的内容,因为它暂时不影响我们实现 useSignal 这个 Hook。

结合 React Function Component 的原理,不难发现控制台的表现恰巧是因为调用 setter 之后更新了 getter 的值,同时更新 State 也触发了组件重新渲染,而组件重新渲染会再一次执行 useSignal Hook,重新调用了 createSignal 创建了一个新的 Signal,之前的 Signal 丢失了。

找到原因之后对症下药,只需要保证每一次重新渲染的时候不重新调用 createSignal 创建新的 Signal,就能保证视图正确更新了。

在 React 中如何保证某个值在多次渲染的时候不发生变化呢?当然是用 useMemo 啦!用 useMemo 包一下 createSignal,保证 Signal 在组件重新渲染的时候不会重新创建:

tsx 复制代码
import { useState, useMemo } from 'react';
import { createSignal } from 'solid-js';

export default function useSignal<T>(value: T) {
  const [, update] = useState(0);
  const [getter, setter] = useMemo(() => createSignal(value), []);

  return [
    getter,
    (value: T) => {
      setter(value);
      update((ticket) => ticket + 1);
    },
  ];
}

现在再次回到 App 组件中,点击 button 就能看到页面上的 count 正确更新了。

大部分同学到这一步就可以关掉本文了,因为这已经实现了标题想要的效果:在 React 中使用 SolidJS 的 Signal。

但还不够,还有优化空间,感兴趣的同学可以继续看下去。

优化

其实在 React 中使用 SolidJS 的 Signal 还有很多种方式,上面的实现只是其中最简单的一种,存在很多小问题,比如调用 setter 时无论 Signal 是否更新都会触发组件重新渲染,触发了不必要的重新渲染,导致组件性能下降。

因此要实现更好的效果,还需要在此基础上进行优化,使 useSignal 只在 getter 的返回值发生变化的时候才触发组件重新渲染。

要实现这样的效果,就不可避免地需要跟踪 getter 的返回值的变化,最简单的办法就是判断一下 valuegetter 的返回值是否相同:

tsx 复制代码
import { useState, useMemo } from 'react';
import { createSignal } from 'solid-js';

export default function useSignal<T>(value: T) {
  const [, update] = useState(0);
  const [getter, setter] = useMemo(() => createSignal(value), []);

  return [
    getter,
    (value: T) => {
      if (getter() === value) return;
      setter(value);
      update((ticket) => ticket + 1);
    },
  ];
}

虽然这种方式非常简单,但并不能很好地帮我们理解 React 和 SolidJS 之间的差异,所以或许应该用一些更能体现两者之间的差异的方式来实现。

还记得上面用到过的 SolidJS 的 createEffect 吗?可以使用 createEffect 包裹 getter 的调用,它会自动跟踪被包裹的 Signal 的变化,并执行 Effect:

tsx 复制代码
import { useState, useMemo } from 'react';
import { createSignal, createEffect } from 'solid-js';

export default function useSignal<T>(value: T) {
  const [, update] = useState(0);
  const [getter, setter] = useMemo(() => createSignal(value), []);

  createEffect(() => {
    getter();
    update((ticket) => ticket + 1);
  });

  return [getter, setter];
}

理论上来说,当 getter 的返回值发生变化时会触发 State 的更新,从而触发组件重新渲染,但是实际上这样的操作会导致组件不停地重新渲染,进入死循环。

因为 createEffect 在创建 Effect 之后会立即执行一次,也就是会立即更新 State,导致组件重新渲染,而组件重新渲染又会重新执行 useSignal 创建新的 Effect,如此往复,周而复始,进入死循环。

为了避免周而复始进入死循环,需要使用 React 的 useRef 创建一个不会被释放的引用,同时使用 SolidJS 提供的更高阶的 API createReaction 将跟踪 getter 变化和触发重新渲染的 update 分离:

tsx 复制代码
import { useState, useMemo, useRef } from 'react';
import { createSignal, createRoot, createReaction } from 'solid-js';

export default function useSignal<T>(value: T) {
  const [, update] = useState(0);
  const [getter, setter] = useMemo(() => createSignal(value), []);
  const root =
    useRef<{ dispose: () => void; track: (fn: () => void) => void }>();

  if (!root.current) {
    root.current = createRoot((dispose) => ({
      dispose,
      track: createReaction(() => update((ticket) => ticket + 1)),
    }));
  }

  root.current.track(() => getter());

  return [getter, setter];
}

到这里 useSignal 就算比较完美地实现了。

最后

为什么会有突发奇想,想在 React 中使用 SolidJS 的 API?其实是前段时间在研究 SolidJS 的时候发现 SolidJS 的 Organization 中有一个叫 React Solid State 的库,允许用户在写 React 的时候使用 SolidJS 提供的 Signal 机制:

GitHub - solidjs/react-solid-state: Auto tracking state management for modern React

在 SolidJS 中,Signal 和 UI renderer 的耦合程度是很低的,因此才能实现这样的能力,与 Anthony Fu 的 reactivue 异曲同工:

GitHub - antfu/reactivue: 🙊 Use Vue Composition API in React components

本文中的一些内容主要参考了以上两个仓库,除了文中写到的几种方式,还有一些与 React 提供的 API 结合更紧密的方式,不过这些都属于"茴香豆的茴有几种写法"的范围了:

StackBlitz - Implement A

StackBlitz - Implement B

StackBlitz - Implement C

相关推荐
前端小万2 分钟前
一次紧急的现场性能问题排查
前端·性能优化
excel18 分钟前
为什么相同卷积代码在不同层学到的特征完全不同——基于 tfjs-node 猫图像识别示例的逐层解析
前端
知识分享小能手19 分钟前
React学习教程,从入门到精通,React 使用属性(Props)创建组件语法知识点与案例详解(15)
前端·javascript·vue.js·学习·react.js·前端框架·vue
用户214118326360221 分钟前
dify案例分享-免费玩转即梦 4.0 多图生成!Dify 工作流从搭建到使用全攻略,附案例效果
前端
CodeSheep21 分钟前
稚晖君又开始摇人了,有点猛啊!
前端·后端·程序员
JarvanMo24 分钟前
Flutter Web vs Mobile:主要区别以及如何调整你的UI
前端
IT_陈寒43 分钟前
Java性能优化:从这8个关键指标开始,让你的应用提速50%
前端·人工智能·后端
天生我材必有用_吴用1 小时前
Vue3+Node.js 实现大文件上传:断点续传、秒传、分片上传完整教程(含源码)
前端
摸鱼的春哥1 小时前
前端程序员最讨厌的10件事
前端·javascript·后端
牧羊狼的狼5 小时前
React 中的 HOC 和 Hooks
前端·javascript·react.js·hooks·高阶组件·hoc