Appearance
第 20 章 引擎与整车:createReactiveSystem
📖 把引擎拆出来
"老墨,"小铃看着满满一屏的 signal.ts,忽然问,"咱们的信号库,现在是一个大整块——连线、盖章、核实、订报人的一生、订阅小组、手动发刊……全在一锅里。真实库也是这样吗?"
老墨笑了:"真实库不是。真实库把代码分成了两半:引擎和整车。"
"引擎……整车?"
"引擎,就是那套最通用的图算法——连接线怎么牵、传播怎么走、怎么核实脏不脏。它不认识'信号''订报人'——它只认识线和章。"
"那'信号''订报人'呢?"
"它们是整车——消息城专属的那一层:公告牌、中转站、订报人的一生……引擎不管这些,整车负责造它们,然后把'该怎么处理每种节点'教给引擎。"
老墨在纸上画:
mermaid
graph TD
subgraph 引擎 engine(src/system.ts)
link["link / unlink(牵线摘线)"]
propagate["propagate(传播盖章)"]
checkDirty["checkDirty(核实)"]
shallow["shallowPropagate(浅传播)"]
end
subgraph 整车 index(src/index.ts)
api["signal / computed / effect / effectScope / trigger ……"]
cb["update / notify / unwatched(引擎的三个插口)"]
end
cb -->|"插进引擎"| link
cb -->|"插进引擎"| propagate
cb -->|"插进引擎"| checkDirty
cb -->|"插进引擎"| shallow"引擎有三个插口:update(重算/消费)、notify(排队订报人)、unwatched(没人订了怎么办)。整车把这三个插口接上,引擎就能动了——一套引擎,可以插到任何整车上去。这就是 createReactiveSystem 的由来:'造一套响应式系统'。"
🎯 本章要解决什么
一句话问题:怎么把信号库拆成引擎(system)和整车(index)——引擎只认线和章,整车负责造消息城?
读完这一章,你会知道:
- 引擎和整车各管什么;
createReactiveSystem工厂:收三个插口,返回五件套(link/unlink/propagate/checkDirty/shallowPropagate);- 文件怎么分:
src/system.ts+src/index.ts(和真实库一模一样的结构)。
💡 概念讲解
引擎的"三插口"
引擎只认线和章,但它有三个地方需要"问整车":
| 插口 | 引擎什么时候问 | 整车怎么答 |
|---|---|---|
update(sub) | 核实的时候:"这个节点,真变了吗?" | 中转站重算一遍;信号对一对新旧 |
notify(sub) | 传播的时候:"这个订报人,排队!" | 把它排进发刊名单(带父订报人、内层先跑) |
unwatched(sub) | 摘线的时候:"它没人订了!" | 中转站歇业、订报人辞职、小组解散 |
引擎问,整车答——引擎不管答案是什么,只管"该问的时候问"。
工厂:createReactiveSystem
ts
const { link, unlink, propagate, checkDirty, shallowPropagate } =
createReactiveSystem({ update, notify, unwatched });createReactiveSystem 是一个工厂:吃进三个插口,吐出一套引擎五件套。整车只需要在开头接一次线,之后到处用五件套干活。
为什么分开
- 和真实库对齐:真实库就是
src/system.ts(引擎)+src/index.ts(整车)——分开了,第 21 章毕业时才能逐字对照; - 引擎可以复用:想造一套"另一种 API 的信号库"?不用重写算法,把引擎拿过来,接上自己的三个插口就行(真实库的 README 就教这个)。
✍️ 动手写代码
今天的活是拆家——把 signal.ts 拆成两个文件。拆完,行为一个字不变,48 个测试(对我们 _dev 来说)继续全绿。
不想一步步抄?
这章的代码量不小。先照着抄 system.ts,再看"完整代码"里的 index.ts 改动清单。抄到一半乱了,直接跳"章末完整代码"。
第 1 步:建引擎(src/system.ts)
新建 src 文件夹里的 system.ts,把"图纸 + 六面旗 + 五件套"搬进去,包成一个工厂:
ts
// src/system.ts —— 信号库的引擎(第 20 章)
// 引擎只认"线"和"章"——它不认识信号/订报人。
// 三个插口(update/notify/unwatched)由整车提供。
// 小旗子(标志):告诉引擎"现在是什么状态"
export const enum ReactiveFlags {
None = 0, // 什么都没有
Mutable = 1, // 可变
Watching = 2, // 盯着
RecursedCheck = 4, // 正在跑
Recursed = 8, // 递归记号
Dirty = 16, // 过期
Pending = 32, // 待核实
HasChildEffect = 64,// 我有孩子
}
// 节点:消息城里的一切,都是节点
export interface ReactiveNode {
deps?: Link; // 我的依赖链(链头)
depsTail?: Link; // 我的依赖链(链尾)
subs?: Link; // 我的听众链(链头)
subsTail?: Link; // 我的听众链(链尾)
flags: ReactiveFlags; // 我的小旗子
}
// 连接线:一笔订阅(线上贴着期号)
export interface Link {
version: number; // 期号
dep: ReactiveNode; // 被依赖的(消息源)
sub: ReactiveNode; // 依赖的(订阅者)
prevDep?: Link; // 依赖链上的前一根线
nextDep?: Link; // 依赖链上的后一根线
prevSub?: Link; // 听众链上的前一根线
nextSub?: Link; // 听众链上的后一根线
}
// 工厂:吃进三个插口,吐出一套引擎五件套
export function createReactiveSystem({
update,
notify,
unwatched,
}: {
update(sub: ReactiveNode): boolean;
notify(sub: ReactiveNode): void;
unwatched(sub: ReactiveNode): void;
}) {
return { link, unlink, propagate, checkDirty, shallowPropagate };
// 连线:能复用旧线就复用(认"期号")
function link(dep: ReactiveNode, sub: ReactiveNode, version: number): void {
// ……第 14 章那份,原样搬来……
}
// 摘线:没人听它了,问整车"怎么办"(unwatched)
function unlink(link: Link): void {
// ……第 15 章那份,原样搬来……
// (末尾的 unwatched(dep) 改成 unwatched(dep) —— 插口直接调用)
}
// 传播:边盖章边下传(认"正在跑的自己")
function propagate(link: Link, innerWrite: boolean): void {
// ……第 13 章那份,原样搬来……
// (notify(sub as EffectNode) 改成 notify(sub) —— 插口直接调用)
}
// 核实:沿着依赖链挨个问"你们真的变了吗?"
function checkDirty(node: ReactiveNode): boolean {
// ……第 12 章那份,原样搬来……
// (update(dep as SignalNode | ComputedNode) 改成 update(dep) —— 插口直接调用)
// ((dep as ComputedNode).deps 改成 dep.deps —— deps 本来就在图纸上)
}
// 浅传播:给一层听众的"待核实"章升级成"过期"章
function shallowPropagate(link: Link): void {
// ……第 9 章那份,原样搬来……
}
// 这根连接线还连在吗?
function isValidLink(checkLink: Link, sub: ReactiveNode): boolean {
// ……第 13 章那份,原样搬来……
}
}搬家的三处小改
引擎里的代码几乎全是原样搬来,只有三处改成"问插口":
unlink末尾:unwatched(dep as ……)→unwatched(dep)(插口直接收ReactiveNode);propagate里:notify(sub as EffectNode)→notify(sub);checkDirty里:update(dep as ……)→update(dep),(dep as ComputedNode).deps→dep.deps。
第 2 步:把 signal.ts 改成整车(src/index.ts)
把 signal.ts 改名成 index.ts,然后做三件事:加导入、删引擎、接插口。
① 顶部加导入——从引擎里拿图纸、旗子和工厂:
ts
// src/index.ts —— 信号库的整车(第 20 章)
// 消息城专属的一层:公告牌、中转站、订报人……引擎的五件套从 system.ts 拿。
import { createReactiveSystem, ReactiveFlags, type Link, type ReactiveNode } from './system';② 删掉引擎部分——这些内容已经搬去 system.ts,从 index.ts 里整段删掉:
ReactiveFlags枚举;ReactiveNode、Link两张图纸;link、unlink、propagate、checkDirty、shallowPropagate、isValidLink六个函数。
③ 在全局状态后面接插口——用工厂把三插口插进引擎,拿回五件套:
ts
// 插插口:把"该怎么做"告诉引擎,拿回引擎五件套
const { link, unlink, propagate, checkDirty, shallowPropagate } =
createReactiveSystem({ update, notify, unwatched });其余全部保留:节点类型(SignalNode/ComputedNode/EffectNode/EffectScopeNode)、全局状态、purgeDeps、update/notify/unwatched(三个插口的实现)、flush/runCleanup/disposeAllDepsInReverse/dispose/disposeScope/run、以及全部的 API(signal/computed/effect/effectScope/trigger/startBatch/endBatch/getBatchDepth/getActiveSub/setActiveSub/untracked)。
插口的签名
update/notify/unwatched 的实现一个字都不用改——引擎的插口收的是 ReactiveNode,TypeScript 的"方法双向兼容"允许窄签名填宽插口。如果 tsc 报了类型错误,把插口实现的首个参数放宽成 ReactiveNode 即可。
第 3 步:改测试的导入
所有测试文件里,把 from '../src/signal' 改成 from '../src/index'。只有 recursion.spec.ts 特殊——它还要从引擎拿旗子(和真实库的测试一模一样的姿势):
ts
import { signal, effect, getActiveSub } from '../src/index';
import { ReactiveFlags } from '../src/system';第 4 步:叫检查员!(回归)
bash
npm test🚀 现在跑一下,你应该看到:
✓ 全部 14 个测试文件、42 条测试,一个都不红。42 个,全绿。 拆家完成——引擎和整车分家了,行为一个字没变。🎉
🧰 TS 小课堂:工厂函数和"插口"
今天有两个新词:工厂函数和依赖注入。
工厂函数
createReactiveSystem 是一个工厂函数——"吃进参数,造出新东西":
ts
createReactiveSystem({ update, notify, unwatched });
// 吃进三个插口 → 吐出一套引擎五件套工厂和普通函数的区别:普通函数干活,工厂函数造东西(常常是造一组有关联的东西,还一起带回来)。咱们已经见过一个工厂了——signal(1) 本身也是工厂:吃进初始值,造出一个"公告牌"函数。
依赖注入(插口)
引擎有 update/notify/unwatched 三个插口——它自己不实现"怎么重算""怎么排队""怎么辞职",全靠整车提供。这叫依赖注入:把"怎么做"的依赖,从外面"注入"进来。
mermaid
graph LR
engine["引擎:该问的时候问 🤔"] --> ask["update? notify? unwatched?"]
ask --> answer["整车:我来答 ✅<br/>重算/排队/辞职"]好处:引擎不认识信号、订报人、小组——它只认线和章。换一套 API(比如 .get()/.set() 风格),插口换了,引擎一行不用改。
📚 消息城词典
| 词 | 意思 | 记住它 |
|---|---|---|
| 引擎 (system) | 最通用的图算法:牵线、传播、核实 | 只认线和章 |
| 整车 (index) | 消息城专属 API:信号、订报人、小组…… | 负责造节点、答插口 |
| 插口 (callback) | 引擎问整车"怎么办"的三个口 | update / notify / unwatched |
| 工厂 (factory) | 吃进参数、造出新东西的函数 | createReactiveSystem |
引擎/整车是正式消息城术语,会同步进附录·消息城词典。
📦 章末完整代码
这一章你的 signal-book/src 文件夹长这样(多了一个文件、改了一个名):
signal-book/
├── demo/
│ └── ch01.mjs
├── node_modules/
├── package.json
├── tsconfig.json
├── src/
│ ├── money.ts
│ ├── system.ts ← 新的:引擎(五件套 + 图纸 + 旗子)
│ ├── index.ts ← 改名来的:整车(消息城 API)
│ └── signal.ts ← 已经不存在了(改名成 index.ts)
└── tests/
├── money.spec.ts
├── signal.spec.ts
├── computed.spec.ts
├── batch.spec.ts
├── naive-problems.spec.ts
├── subscription.spec.ts
├── checkdirty.spec.ts
├── checkup.spec.ts
├── recursion.spec.ts
├── cleanup.spec.ts
├── scope.spec.ts
├── trigger.spec.ts
├── batch-lifecycle.spec.ts
└── untracked.spec.tssrc/system.ts 完整内容(第 1 步抄的那份——五件套函数体照搬第 9-15 章的各份):
ts
// src/system.ts —— 信号库的引擎(第 20 章)
// 引擎只认"线"和"章"——它不认识信号/订报人。
// 三个插口(update/notify/unwatched)由整车提供。
// 小旗子(标志):告诉引擎"现在是什么状态"
export const enum ReactiveFlags {
None = 0, // 什么都没有
Mutable = 1, // 可变
Watching = 2, // 盯着
RecursedCheck = 4, // 正在跑
Recursed = 8, // 递归记号
Dirty = 16, // 过期
Pending = 32, // 待核实
HasChildEffect = 64,// 我有孩子
}
// 节点:消息城里的一切,都是节点
export interface ReactiveNode {
deps?: Link; // 我的依赖链(链头)
depsTail?: Link; // 我的依赖链(链尾)
subs?: Link; // 我的听众链(链头)
subsTail?: Link; // 我的听众链(链尾)
flags: ReactiveFlags; // 我的小旗子
}
// 连接线:一笔订阅(线上贴着期号)
export interface Link {
version: number; // 期号
dep: ReactiveNode; // 被依赖的(消息源)
sub: ReactiveNode; // 依赖的(订阅者)
prevDep?: Link; // 依赖链上的前一根线
nextDep?: Link; // 依赖链上的后一根线
prevSub?: Link; // 听众链上的前一根线
nextSub?: Link; // 听众链上的后一根线
}
// 工厂:吃进三个插口,吐出一套引擎五件套
export function createReactiveSystem({
update,
notify,
unwatched,
}: {
update(sub: ReactiveNode): boolean;
notify(sub: ReactiveNode): void;
unwatched(sub: ReactiveNode): void;
}) {
return { link, unlink, propagate, checkDirty, shallowPropagate };
// 连线:能复用旧线就复用(认"期号")
function link(dep: ReactiveNode, sub: ReactiveNode, version: number): void {
const prevDep = sub.depsTail;
if (prevDep !== undefined && prevDep.dep === dep) {
return; // ① 刚连的就是它:不用再连
}
const nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps;
if (nextDep !== undefined && nextDep.dep === dep) {
nextDep.version = version; // ② 旧线还用得着:贴上期号,复用!
sub.depsTail = nextDep;
return;
}
const prevSub = dep.subsTail;
if (prevSub !== undefined && prevSub.version === version && prevSub.sub === sub) {
return; // ③ 依赖那边已经连着同一期了:不用再连
}
const newLink: Link = {
version,
dep,
sub,
prevDep,
nextDep,
prevSub,
nextSub: undefined,
};
if (prevDep !== undefined) {
prevDep.nextDep = newLink;
} else {
sub.deps = newLink;
}
if (nextDep !== undefined) {
nextDep.prevDep = newLink;
}
sub.depsTail = newLink;
if (prevSub !== undefined) {
prevSub.nextSub = newLink;
} else {
dep.subs = newLink;
}
dep.subsTail = newLink;
}
// 摘线:没人听它了,问整车"怎么办"(unwatched)
function unlink(link: Link): void {
const { dep, sub, prevDep, nextDep, prevSub, nextSub } = link;
if (prevDep !== undefined) {
prevDep.nextDep = nextDep;
} else {
sub.deps = nextDep;
}
if (nextDep !== undefined) {
nextDep.prevDep = prevDep;
} else {
sub.depsTail = prevDep;
}
if (prevSub !== undefined) {
prevSub.nextSub = nextSub;
} else if ((dep.subs = nextSub) === undefined) {
unwatched(dep); // 没人听它了:问整车怎么办
}
}
// 传播:边盖章边下传(认"正在跑的自己")
function propagate(link: Link, innerWrite: boolean): void {
const tickets: Link[] = []; // 回程票
let l: Link | undefined = link; // 现在站在哪根线上
while (l !== undefined) {
const next: Link | undefined = l.nextSub; // 同层的下一站(先记住)
const sub: ReactiveNode = l.sub;
let flags = sub.flags;
if (!(flags & (ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed | ReactiveFlags.Dirty | ReactiveFlags.Pending))) {
sub.flags = flags | ReactiveFlags.Pending;
if (innerWrite) {
sub.flags |= ReactiveFlags.Recursed;
}
} else if (!(flags & (ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed))) {
flags = ReactiveFlags.None;
} else if (!(flags & ReactiveFlags.RecursedCheck)) {
sub.flags = (flags & ~ReactiveFlags.Recursed) | ReactiveFlags.Pending;
} else if (!(flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) && isValidLink(l, sub)) {
sub.flags = flags | ReactiveFlags.Recursed | ReactiveFlags.Pending;
flags &= ReactiveFlags.Mutable;
} else {
flags = ReactiveFlags.None;
}
if (flags & ReactiveFlags.Watching) {
notify(sub); // 问整车:排队
}
if (flags & ReactiveFlags.Mutable) {
const subSubs: Link | undefined = sub.subs;
if (subSubs !== undefined) {
if (next !== undefined) {
tickets.push(next);
}
l = subSubs;
continue;
}
}
l = next ?? tickets.pop();
}
}
// 核实:沿着依赖链挨个问"你们真的变了吗?"
function checkDirty(node: ReactiveNode): boolean {
type Ticket = { link: Link; dep: ReactiveNode };
const tickets: Ticket[] = [];
let l: Link | undefined = node.deps;
let dirty = false;
while (true) {
if (l === undefined) {
if (tickets.length === 0) {
return dirty;
}
const ticket: Ticket = tickets.pop()!;
if (dirty) {
if (update(ticket.dep)) {
continue;
}
dirty = false;
} else {
ticket.dep.flags &= ~ReactiveFlags.Pending;
}
l = ticket.link.nextDep;
continue;
}
const dep: ReactiveNode = l.dep;
if (dep.flags & ReactiveFlags.Dirty) {
if (update(dep)) {
dirty = true;
l = undefined;
} else {
l = l.nextDep;
}
} else if (dep.flags & ReactiveFlags.Pending) {
tickets.push({ link: l, dep });
l = dep.deps;
} else {
l = l.nextDep;
}
}
}
// 浅传播:给一层听众的"待核实"章升级成"过期"章
function shallowPropagate(link: Link): void {
let l: Link | undefined = link;
while (l !== undefined) {
const sub = l.sub;
if ((sub.flags & (ReactiveFlags.Pending | ReactiveFlags.Dirty)) === ReactiveFlags.Pending) {
sub.flags = sub.flags | ReactiveFlags.Dirty;
}
l = l.nextSub;
}
}
// 这根连接线还连在吗?
function isValidLink(checkLink: Link, sub: ReactiveNode): boolean {
let l = sub.depsTail;
while (l !== undefined) {
if (l === checkLink) {
return true;
}
l = l.prevDep;
}
return false;
}
}src/index.ts 的全部内容 = 第 19 章的 signal.ts,按第 2 步做了三处手术:加导入、删引擎、接插口。其余(节点类型、purgeDeps、三插口实现、flush/run/dispose/disposeScope、全部 API)一字未动。
测试的导入改动:全部 '../src/signal' → '../src/index',recursion.spec.ts 额外加一行 import { ReactiveFlags } from '../src/system';。
✅ 跑通了? 只要
npm test打出42 passed,引擎和整车就分家了——引擎谁都能插,整车专属消息城。
🎮 动手试试
怎么玩
先自己写答案,再点开对照。忍得住才点开哦!
1. 预言输出(猜一猜)
小铃想试试"引擎不知道消息城"到底是什么意思。她问:如果引擎的 notify 插口接的是一个空函数(notify: () => {}),会发生什么?
👉 点开看答案
订报人永远不会排队——消息变了,谁也不醒。
propagate 传播时,遇到"盯着"的节点就调 notify(sub)——可插口是空的,什么也没发生。引擎照样走完全程(盖章、下钻都正常),只是**"排队"这一步没人接**。
这正说明引擎和整车各管各的:引擎负责'该通知的时候调 notify',整车负责'notify 到底干什么'。插口空着,引擎不知道、也不管——它只保证"该问的时候问了"。
2. 帮小铃找 bug(抓错)
小铃把代码搬进 system.ts 时,还留着旧写法:
ts
if (flags & ReactiveFlags.Watching) {
notify(sub as EffectNode); // 咦?EffectNode 在 system.ts 里不存在!
}结果 tsc 报错:'EffectNode' 不存在。为什么?应该怎么写?
👉 点开看答案
因为 EffectNode 是整车的图纸(在 index.ts 里),引擎里没有它。
引擎只认 ReactiveNode——插口的类型就是 notify(sub: ReactiveNode)。所以直接写:
ts
notify(sub); // 插口直接收 ReactiveNode引擎不该知道"订报人"长什么样——它只要把"这个节点在盯着"传给插口,怎么处理是整车的事。
3. 填空(补全)
index.ts 里接插口少了一样东西。补全它:
ts
const { link, unlink, propagate, checkDirty, shallowPropagate } =
createReactiveSystem({ /* 这里该写什么? */ });👉 点开看答案
写 update, notify, unwatched:
ts
const { link, unlink, propagate, checkDirty, shallowPropagate } =
createReactiveSystem({ update, notify, unwatched });三个插口就是整车的三个"答话人":update 答'真变了吗'、notify 答'谁排队'、unwatched 答'没人订了怎么办'。不接全,引擎就有口没人答。
4. 小挑战(迷你整车)
不造完整 API——只验证引擎能独立工作。用 createReactiveSystem 配上三个"空插口",再手动牵一根线:
ts
import { createReactiveSystem, ReactiveFlags, type ReactiveNode } from '../src/system';
const engine = createReactiveSystem({
update: () => false,
notify: () => {},
unwatched: () => {},
});
const a: ReactiveNode = { flags: ReactiveFlags.Mutable };
const b: ReactiveNode = { flags: ReactiveFlags.Mutable };
engine.link(a, b, 1);
// 验证:b 的依赖链上,第一根线就是 a
expect(b.deps?.dep).toBe(a);写进测试跑一下——这就是"引擎插到任何整车都能用"的最小证明。
👉 点开看答案
核心就是上面那份代码。跑起来应该全绿:
engine.link(a, b, 1)牵一根线:a的听众链、b的依赖链都挂上它;expect(b.deps?.dep).toBe(a)——b依赖a✓;- 三个空插口说明:引擎只做"牵线","牵完线之后干什么"完全交给插口——空着也能牵线,插上就能全自动。
这就是依赖注入的威力:引擎不知道消息城,消息城也能把它用起来。
🏁 本章小结
- 信号库拆成了引擎(system:牵线、传播、核实的通用图算法)和整车(index:消息城专属 API);
createReactiveSystem是工厂:吃进三个插口(update/notify/unwatched),吐出一套五件套——引擎只问,整车只答;- 文件结构和真实库对齐(
src/system.ts+src/index.ts)——42 个测试全绿,行为一字没变。
引擎和整车分家了。老墨把最后一张图纸拍在桌上,上面写着两个字:
毕业。
下一章,就是全书的大结局:跑绿真实库的全部测试,和朴素版比一比速度——消息城的信号库,终于要毕业了。