React中如何模拟Solid.js与Preact Signals信号机制?

来源:程序开发作者:布兰登头衔:网络博主
导读:本期聚焦于小伙伴创作的《React中如何模拟Solid.js与Preact Signals信号机制?》,敬请观看详情。传统React状态更新依赖组件重渲染,在深层嵌套结构中容易引发不必要的性能损耗。Signals是一种细粒度响应式原语,Solid.js借助它实现按需更新,Preact也推出了Preact Signals库。若在React里复用这套思路,可通过useSyncExternalStore订阅外部信号源,或用自定义store配合订阅函数绕过虚拟DOM diff。两种方式都能把变更锁定到具体DOM节点,减少父组件渲染次数。理解信号读写分离、依赖追踪与批处理,有助于在复杂表单与实时面板中落地更轻量的状态方案。

在React生态中,状态管理长期围绕useState与useReducer展开,每次更新几乎都会触发组件函数重新执行。Solid.js的Signals与Preact Signals提出了一种不同的响应式模型:信号值的变化直接通知依赖它的具体视图,而不是让整个组件树参与协调。这种机制在React中并非原生支持,但我们可以通过外部存储与订阅模式进行模拟,从而在保留React渲染管线的同时获得细粒度更新能力。

React中如何模拟Solid.js与Preact Signals信号机制?

Signals的核心原理与React渲染模型的冲突

Signals本质上是一个具备读写分离能力的值容器。当代码读取信号时,运行时会记录当前正在执行的副作用或计算函数;当信号被写入,仅那些真正读取过它的副作用会被标记为脏并调度更新。Solid.js在编译阶段将组件转化为命令式DOM操作,因此信号能直接驱动文本节点或属性修改。Preact Signals则利用Preact轻量虚拟DOM的差异定位,在信号变更时只更新对应组件实例。

React的渲染模型与此不同。它的函数组件在每次渲染时都会重新运行,JSX返回的虚拟DOM会被调和。如果我们在组件内部直接读取一个外部信号而不订阅,React并不知道这个信号变化需要触发重渲染。因此模拟Signals的第一要务,是让React的调度系统感知到信号变更,同时尽量避免父组件的无谓执行。这就引出了依赖追踪与渲染边界两个关键问题。

另一个冲突点在于批处理。React 18引入了自动批处理,但外部信号的写入往往发生在React事件系统之外,例如定时器或第三方库回调。若不手动批处理,可能出现一次信号变更触发多次独立渲染。理解这些底层差异,是我们在React中正确模拟Signals的前提,而不是简单封装一个get/set对象。

基于useSyncExternalStore的React Signals模拟

React 18提供的useSyncExternalStore是连接外部可变源与React渲染的安全通道。我们可以构建一个信号工厂,每个信号包含getSnapshotsubscribeset方法。组件通过hook订阅信号,仅当该信号的快照变化时,使用组件的局部渲染才会被调度。这种方式不会让父组件重渲染,除非父组件自己也订阅了同一信号。

下面给出一个最小实现。我们定义一个创建信号的函数,并用模块级Map保存订阅者。写入时遍历订阅者通知变更,React的useSyncExternalStore会比较快照决定是否重渲染。注意快照必须是引用稳定或值相等的,否则会造成无限循环。

function createSignal(initial) {
  let value = initial;
  const subscribers = new Set();
  return {
    get() { return value; },
    set(next) {
      value = next;
      subscribers.forEach(fn => fn());
    },
    subscribe(fn) {
      subscribers.add(fn);
      return () => subscribers.delete(fn);
    }
  };
}

const countSignal = createSignal(0);

function Counter() {
  const count = React.useSyncExternalStore(
    countSignal.subscribe.bind(countSignal),
    countSignal.get.bind(countSignal)
  );
  return React.createElement('button', {
    onClick: () => countSignal.set(count + 1)
  }, 'count: ' + count);
}

上述代码在React中实现了最基本的信号订阅。它的优势是契合React官方推荐的外部存储模式,不会破坏并发特性。缺点是每个信号需要单独订阅,且若多个信号组合成派生状态,需要自行用useMemo或计算信号封装。相比Solid.js编译期优化,这种运行时订阅在极高频率更新时仍有轻微开销。

为了更接近Preact Signals的computed能力,我们可以扩展信号工厂,增加依赖收集逻辑:当计算信号被读取时,自动订阅其源信号,并在源变更时重算。这样在React中也能写出类似const double = computed(() => count.get() * 2)的声明式派生,且只影响使用double的组件。

Preact Signals在React项目中的桥接实践

Preact官方提供了@preact/signals-react桥接包,它内部利用Babel插件将signal.value的读取自动包裹为订阅。在React中使用时,开发者写法和Preact几乎一致:声明const count = signal(0),在JSX中通过count.value读取。插件会在编译阶段插入useSyncExternalStore调用,从而让React感知变更。

如果没有配置Babel插件,也可以手动用useSignaluseComputed hook达到类似效果。桥接方案的好处是成熟稳定,社区已处理批处理与SSR兼容。例如在Next.js中,信号状态可借助useSignal在客户端激活,避免 hydration 不匹配。但它要求构建链支持对应转换,对纯CRA或Vite项目需额外配置。

import { signal, computed } from '@preact/signals-react';

const width = signal(800);
const height = signal(600);
const area = computed(() => width.value * height.value);

function Panel() {
  return (
    <div>
      <p>面积: {area.value}</p>
      <button onClick={() => width.value += 10}>加宽</button>
    </div>
  );
}

从架构角度看,在React中模拟Signals并不意味着取代Redux或Context。信号更适合局部高频状态,如表单输入、拖拽坐标、动画进度。将这些状态移出React内部state,可以显著降低大型列表的渲染成本。实际落地时,建议先用量化工具测量组件重渲染次数,再决定是否引入信号方案,避免过早优化带来的复杂度上升。

综合来看,Solid.js的细粒度更新依赖编译手段,Preact Signals依靠轻量运行时与构建插件,而在React中我们主要通过useSyncExternalStore与外部存储契约来逼近该模型。掌握信号读写、订阅与批处理三要素,就能在现有React工程中安全地享受Signals带来的性能红利,而不必迁移整个技术栈。

ReactSignalsSolid_js修改时间:2026-08-13 18:42:36

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。