计算状态
选择冻结 getter 快照、显式路径或可管理生命周期的深层派生 selector。
Coaction 的 computed value 与字段追踪共用同一张 signal graph。组件可以读取缓存 getter,getter 再读取 store 字段,失效过程沿依赖图传播,无需手工维护 memo 数组。
访问器 getter
普通 getter 是默认的派生状态方式:
type Item = { price: number; quantity: number };
const cart = create((set) => ({
items: [] as Item[],
taxRate: 0.08,
get subtotal() {
return this.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
},
get total() {
return this.subtotal * (1 + this.taxRate);
},
add(item: Item) {
set(() => {
this.items.push(item);
});
}
}));subtotal 在 items 变化前保持缓存;total 依赖缓存后的 subtotal 与 taxRate,关系会自动发现。
Getter 应保持纯函数,不应调用 setState()、修改外部状态,或依赖隐藏副作用产生不同结果。
使用 get() 声明显式依赖
store factory 的第二个参数可手工声明依赖:
const cart = create((set, get) => ({
items: [] as Item[],
total: get(
(state) => [state.items] as const,
(items) => items.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
}));当显式声明让跨 slice 逻辑更清楚,或集成代码不应依赖隐式 getter read 时,使用这种形式。
无参数调用 get() 会返回当前根状态。在 slice factory 中,它适合读取另一 namespace,但更新仍必须通过 set()。
追踪粒度
原生 getter 与 get(deps, fn) 读取冻结快照,依赖以 store/slice 字段为边界。因此 getter 中的 this.user.name 会因 user.age 变化而重新计算。使用 get(s => [s.user.name], fn) 时,依赖 selector 会重跑,但 name 不变时可以复用 fn 的结果。计算函数重跑和通知消费者是两件事:相同的 primitive 结果可以阻止下游执行。
React selector 与 observer 使用可回收的深层路径追踪。读取 state.user.name 的 selector 可以忽略仅改变 age 的 recipe。父对象 replacement、数组结构变化和 graph snapshot patches 可能扩大失效范围。
可管理生命周期的派生读取
可选入口 coaction/derived 提供归属 store 的缓存读取,不改变原生 getter 默认行为,也不会把这套运行时代码加入默认入口的 consumer bundle:
import { create } from 'coaction';
import { derive, derivePath } from 'coaction/derived';
const person = create({ user: { name: 'Ada', age: 30 } });
// 精确数据路径,不收集中间对象的依赖。
const name = derivePath(person, ['user', 'name']);
// 动态选择,按叶子和结构收集依赖。
const greeting = derive(person, (s) => `Hello, ${s.user.name}`, {
deep: true
});
name(); // 'Ada'
greeting(); // 'Hello, Ada'
person.setState((s) => {
s.user.age++;
});
greeting(); // name 未变,复用缓存
name.dispose();
greeting.dispose();
person.destroy();derivePath 创建时复制路径,支持 string/number/symbol key;数据不存在时返回 undefined。它只选择 state data,不选择 action 或原生 computed getter。动态路径和 getter 组合请使用 derive。这两个 API 都不会把任意 reduce、filter 或 sort 自动变成增量算法。
对象身份是独立依赖
derive(store, selector) 默认保留所有经过的对象身份依赖。{ deep: true } 显式选择值语义:为了读取子属性而经过父对象,不会自动保留父对象身份。JavaScript 的 ===、Object.is 和 WeakMap 查询无法通过 property trap 识别,需要显式标记:
import { derive, identity } from 'coaction/derived';
const captured = person.getState().user;
const description = derive(
person,
(s) => {
const user = identity(s.user);
return `${user === captured}:${user.name}`;
},
{ deep: true }
);age 变化会替换 user 对象,因此这里即使 name 不变也必须重算。identity() 返回原本的 readonly 对象;whole() 则为整体扫描返回自有 raw data,不能修改它。
直接返回 state 对象,或将它放进普通对象/数组的数据属性中,会自动保留 terminal dependency;带 cycle 的结果包装也受支持。追踪不会执行结果的 accessor,也不会遍历 opaque instance、函数或 closure;在这些位置携带 state 时,应在 selector 内用 identity() 标记。数组、空洞、symbol、null prototype、cycle 和共享引用保留现有 local-state contract。Date、Map、Set 和 class instance 仍是 atomic identity leaf,其内部 mutation 不会自动变得可观察。
结果比较
默认使用 Object.is,包括 NaN 与正负零的区别。新对象即使字段相同,也属于不同结果;可以用 equals 显式复用等价派生数据:
const adult = derive(person, (s) => ({ adult: s.user.age >= 18 }), {
deep: true,
equals: (before, after) => before.adult === after.adult
});结果比较与依赖收集独立;比较函数应保持纯函数,其读取不会添加响应式依赖。即使复用结果,分支依赖也会重新协调。结果携带的 state 对象发生变化时仍会通知,包括身份稳定的 root/slice facade,避免 equals 隐藏实际 state 变化。默认不对任意 local graph 做结构深比较。
事务、归属与 React
recipe 内的读取使用当前 store 的 draft,不更新已提交缓存,也不保留 draft proxy。recipe 抛错或 validator 拒绝后,已提交缓存保持有效;消费者在 commit 被接受后失效。selector 抛错后,缓存读取仍抛错,输入依赖变化后重新求值。可组合归属同一 store 的派生读取;这个 API 不负责跨 store 的事务协调。selector 必须是同步纯读取;await 后的工作不在追踪范围内,这不是 Resource/Suspense API。
每个读取保留当前依赖,直到调用 dispose() 或 store.destroy()。尚未读取的派生对象不会开启 patches。dispose() 可重复调用,会释放该读取的依赖与缓存,之后再次读取会抛错。停止某个消费者不会销毁 store 归属的派生对象,其他消费者仍可继续使用;原生 getter 的缓存仍采用其既有 store 生命周期。
请随 store 或另一个明确 owner 创建派生对象,不要在 React render 中创建,也不要把 useMemo 当成清理边界,否则废弃的 render 可能留下实例。已存在的 store 归属读取可以用于 selector 或 observer:
const displayName = derive(usePerson, (s) => s.user.name, { deep: true });
function Name() {
return <span>{usePerson(() => displayName())}</span>;
}这里 usePerson 来自 @coaction/react。owner 结束时销毁 displayName,或销毁其 store;组件卸载会释放 React 订阅。SSR 的 store 和派生读取归属对应请求;shared client 使用已接受的镜像状态重新计算。派生定义和缓存不会进入 patches、history 或 transport payload。
独立信号原语
coaction 导出的 computed、signal、effect 仍是 alien-signals 原语,适合独立 signal graph,但不具备 store 归属的销毁和 draft-aware 缓存。独立 computed 保守保留对象依赖;若结果是整个 root facade,需要显式 whole(store.getState()) 读取来登记整体依赖。它们的 Coaction 路径记账持续到 store 生命周期结束;需要回收动态 store 依赖时,请使用 derive。初始嵌套数据中的普通 getter 仍会被求值成 data,不会递归注册成 computed 定义。
外部适配器
原生 getter computed 属于 Coaction 自有状态。外部 adapter instance 不会自动获得 Coaction 缓存 getter 或 get(deps, selector) 语义;应由其自身运行时(MobX computed、Pinia getter、selector 或 machine state)负责派生状态。derive 和 derivePath 会明确拒绝外部 adapter。
性能预期
稳定重复读取是缓存 getter 最强的路径。更新后,Coaction 仍需维护 readonly public-state 边界,并在下一次派生读取前刷新 computed 使用的 snapshot。微基准应被视为回归信号,而非普适结论;维护命令与解释见性能指南。
深层追踪适合稀疏读取。全量集合扫描继续使用读取冻结快照的原生 getter,也可以用 derive(store, s => s.total) 组合该 getter。若通过深层 proxy 遍历每个元素,只会增加追踪成本,不能省去求值。不可变更新仍需复制变化路径上的容器,flat array 的单点写入并不独立于数组长度。
pnpm benchmark:derived 覆盖 1k/10k/50k 稀疏读取、10k 全量扫描、稳定缓存读取,以及尚未读取/已销毁派生对象的写入成本;已加入 benchmark:check。