Appearance
为什么“Getter 函数 + computed”依然不会自动更新?
本节目标:理解“为什么“Getter 函数 + computed”依然不会自动更新?”的核心思路,并能把它用于实际开发或面试表达。 为什么“Getter 函数 + computed”依然不会自动更新? Signal.Computed 只有在依赖的其他 Signal 发生 .set() 时才会重新计算。 一个普通的 Getter 函数(() => props.value)不是 Signal,也没有订阅/发布能力。computed(() => resolve(getter)) 只是在执行那个函数、拿到一个数值后就完事了——后续你再调用那个 Getter,它的返回值虽然变了,但没人去触发 computed 的重新求值。 结论:要让核心逻辑自动响应,输入必须本身是 Signal,或者由框架适配层在外面把变化写回 Signal。
跨框架的主流做法
| 层级 | 作用 | 典型实现 |
| 核心库(纯 JS) | 只依赖 @preact/signals / signal-polyfill 或你自研的信号实现。暴露 setOptions / setValue 等 API,用 .set() 触发内部更新。 | useDataItem、TanStack Table Core、XState、Chart.js Core … |
| 适配层(Vue / React / Solid / Svelte …) | 负责把各自框架的响应式系统 同步到核心库的 Signal: | |
| ① 初次创建时构造 Signal.state(init); | ||
| ② 用 watch / useEffect / createEffect 等监听 props/refs 变化,调用 .set(newVal)。 | @tanstack/react-table, @vueuse/core/useSignals, solid-js/web … |
这样既保证核心库零依赖、跨框架,又能在每个 UI 框架里得到实时更新。
具体改造方案1 核心 useDataItem(保持不变)
ts 复制编辑 // ✅ 依旧要求真正会变的字段是 Signalexport interface DataItemOptionsCore { value: Signal.State<number>; loading?: Signal.State<boolean>; // 其余字段同理……} (你现在的实现已经符合这一点,只需文档里明确「请传 Signal 而不是 Getter」即可。) 2 Vue 适配层示例
ts 复制编辑 import { ref as vRef, watch } from 'vue'import { Signal } from 'signal-polyfill'import { useDataItem } from '../core/useDataItem' export function useDataItemVue(props: { value: number }) { // ① 创建 Signal const valueSig = Signal.state(props.value) // ② watch Vue prop → Signal watch( () => props.value, v => valueSig.set(v) ) // ③ 把 Signal 交给核心 return useDataItem({ value: valueSig })} 3 React 适配层示例
tsx 复制编辑 import { useEffect, useMemo } from 'react'import { Signal } from 'signal-polyfill'import { useDataItem } from '../core/useDataItem' export function useDataItemReact({ value }) { // 用 useMemo 保证 Signal 只创建一次 const valueSig = useMemo(() => Signal.state(value), []) // 同步 prop -> Signal useEffect(() => valueSig.set(value), [value]) return useDataItem({ value: valueSig })} 4 为什么不用 Getter 当依赖?
- Getter 在 JS 层面只是一个普通函数;核心库无法感知它“最近被谁调用、值何时改变”。
- 把“监听变化”这件事留给适配层,才能用各自框架最擅长、最高效的机制(Vue 的 watch、React 的重新渲染、Solid 的 fine-grained reactive 等),同时核心代码无需关心每个框架的实现细节。
- 这正是 TanStack、RxJS、SWR、Jotai 等库采用的分层模式。
需要的完整文件types.ts
ts 复制编辑 import { Signal } from 'signal-polyfill' export interface DataItemOptions { /* 必须是 Signal.State,这样核心才能追踪变化 / value: Signal.State<number> loading?: Signal.State<boolean> / 其他配置可以继续用字面量或 Signal,按需选择 */ decimals?: number | Signal.State<number> // ...} export enum ThresholdStatus { None='none', Up='up', Down='down', Equal='equal' } useDataItem.ts 与你上一条的版本完全一致——不需要改;关键只是接口期望 Signal 而不是 Getter。 核心逻辑略去,此处标注:
ts 复制编辑 export function useDataItem(opts: DataItemOptions) { // 直接 .get() 就能追踪 const value = computed(() => opts.value.get()) // ...}
总结:最佳实践
- 核心层 依赖信号(Signal.State),用 .get() / .set() 完成依赖追踪和变更发布。
- 框架适配层 把各自的响应式(Vue watch、React useEffect…)同步到核心的 Signal。
- 不要把普通函数 Getter 当作响应式源——它们没有订阅能力。
- 这种分层方式可同时服务 Vue、React、Solid、Svelte 等任何框架,正是 TanStack、React Query、SWR 等库的通行做法。
