常见问题
响应链传递
信号必须通过 $ 前缀的变量传递才能保持响应性
根据 Signal Compiler 编译策略,$ 前缀的变量才会被编译编译器识别。
以自定义 hook 为例子:
javascript
let $msg = ""; // 声明
const $dipslay = $msg + "hello"; // 派生
const $useText = ($a) => ({
$text: $a + "hello"
});
// 返回信号
const { $text } = $useText(
// 入参信号
$display
);声明信号 -> 派生信号 -> hook(入参信号) -> hook(返回值信号) -> 派生信号/解构信号这里每一步都会进行信号编译,响应才不会中断,这就是信号的响应链传递。
为什么要加 $ 前缀?
第一:为了编译时将符号 $ 前缀的变量编译成和信号相关的代码所做的标记,这个标记是明确的没有歧义的(避免极少数三方库的冲突,请自行别名处理),编译插件已经开源,具体原理可以看 signal-compiler
第二:为了避免和普通变量(非 Signal)混淆。在大型应用中变量的数量庞大,开发人员无法区分信号变量和普通变量,导致 debug 困难。
effect 内调用“读+写同一信号”的函数会导致死循环
effect 执行期间会搜集所有读取到的信号,包括 effect 内调用的函数体内读取的信号。如果某个函数“先读后写”同一个信号(典型的注册类函数 $list = [...$list, x]),effect 就会依赖上自己写入的信号:写入触发 effect 重跑 → 重跑又写入 → 无限循环,页面事件循环卡死。
tsx
let $messages: string[] = [];
const register = (id) => {
$messages = [...$messages, id]; // 读 $messages(被 effect 追踪)→ 写 $messages → 自触发
};
effect(() => {
if ($valid) register("err"); // ← 发散式自写,死循环
});解决方式:用 untrack 包住注册函数体内的读写,让它们不被当前 effect 搜集为依赖:
tsx
import { untrack } from "j20";
const register = (id) => {
untrack(() => {
$messages = [...$messages, id];
});
};注意:
untrack只影响当前 effect 的依赖搜集,不影响信号变更对其他 effect / 视图的通知,注册结果依然正常触发界面更新。- 收敛式自写(effect 内读+写同一信号、但能自然收敛,如计数器到阈值停止)是受支持的行为,不要用 untrack 破坏它;
untrack只用于发散式的注册/回填类读写。
