Appearance
第 1 章 消息城
📖 小铃的第一天
清晨,阳光刚爬上屋顶,小铃背着鼓鼓的报袋,站在消息城的大门口。
消息城是一座奇怪的城。城里没有汽车,没有地铁,只有消息在跑来跑去。每一家店铺门口都挂着一块公告牌,牌上的字会自己变:蛋糕店挂"今日蛋糕:🍰 12 元",如果面粉涨价了,牌上的数字会自己变成 14 元,没人去擦掉重写。
"这就是信号。"主编老墨推了推眼镜,"消息城的一切,都是靠'一变全变'在运转。"
小铃咽了咽口水。她今天的任务很简单:送报纸,每送一份赚 2 块钱。
"这有什么难的!"小铃心想。她掏出一个小本子,写了两行:
count = 1 // 送出的报纸份数
money = count × 2 // 赚的钱 = 份数 × 2一上午,她送了 1 份报纸,赚了 2 块钱。接着又送出去一份!她高兴地把 count 改成 2,然后愣住了——
小本子上写着:money = 2。
可是,2 份报纸应该赚 4 块钱啊!
"为什么 count 变了,money 不自己跟着变?"小铃冲进主编办公室,"这城里不是说好'一变全变'的吗?"
老墨笑了:"因为我们还缺一样东西——信号。今天,我们就来造它。"
🎯 本章要解决什么
一句话问题:为什么变量变了,靠它算出来的东西不会自己跟着变?
读完这一章,你会知道:
- 什么是信号,消息城为什么靠它运转;
- 什么是依赖和订阅者;
- 亲手跑一个演示,亲眼看看"手工更新"到底有多麻烦。
💡 概念讲解
什么是信号?
信号(signal)就是一条会变化的消息。
你其实天天见到它:
- 手机上的电量:电量变了,百分比数字自己跟着变;
- 游戏里的血量:掉血了,血条自己变短;
- 天气预报的温度:天气变了,度数自己变。
这些"一变,别的东西就自动跟着变"的本事,靠的就是信号。在消息城里,信号是消息源——就像小铃的报纸,一变,所有订报的人都要知道。
什么是依赖?
小铃的本子上写着一句话:money = count × 2。
这句话的意思是:money 依赖 count。count 一变,money 就该重新算。
在消息城里,我们把这种"谁需要谁"的关系画成一张图。箭头从被依赖的指向依赖它的:
mermaid
graph LR
count["count 🗞️ 信号(报纸份数)"] --> money["money 🧮 计算值(赚的钱)"]读法:count 一变,money 要重新算。
这张图太重要了,它有个名字,叫依赖图。全书的每一章,我们都在跟它打交道。
什么是订阅者?
那"订报的人"呢?在消息城,谁在看某条消息,谁就是它的订阅者(sub)。
小铃的本子上,money 就是 count 的订阅者——它在"听"count 的消息。
mermaid
graph LR
count["count 🗞️"] --> money["money 🧮"]
money --> ling["小铃 📝 自动反应<br/>(在听 money 的人)"]等第 3 章我们会看到:程序里也有"订报的人",消息一变,它就会自动重新执行——我们叫它自动反应。
魔法长什么样?
如果信号魔法真的存在,小铃的本子会变成什么样?大概是这样:
js
const count = signal(1); // 造一个信号,初始值 1
const money = computed(() => count() * 2); // 计算值:count 一变,自动重算
effect(() => console.log('赚了', money(), '元')); // 订报的人:自动跟着变只要 count 一改,money 自己变,小铃的本子自己更新,一个字母都不用她动手。
这就是我们 21 章之后要造出来的东西——它不是幻想,是真实存在的库,叫 alien-signals,被全世界很多网站用着。我们要从零把它造出来。
不过,魔法不能一步变出来。今天的任务,是先用最笨的办法,亲眼看看没有魔法的时候有多麻烦。
✍️ 动手写代码
现在轮到你了!这一章我们只做一件事:把小铃的手工更新演示跑起来,亲眼看看问题。
第 1 步:打开终端
终端(也叫"命令行")是一个让你用键盘命令电脑的窗口。不同电脑打开方式不一样,让你的爸爸妈妈帮忙打开一个终端就行。
第 2 步:建一个文件夹
在终端里,输入下面这行命令,按回车:
bash
mkdir signal-book这会在你的电脑上建一个叫 signal-book 的文件夹——你后面 21 章写的所有代码,都会住在这里。
第 3 步:在里面建一个 demo 文件夹
bash
cd signal-book && mkdir democd 是"走进文件夹",mkdir 是"新建文件夹"。这两兄弟你以后会天天见。
第 4 步:写演示文件
在 signal-book/demo 文件夹里,新建一个文件,名字叫 ch01.mjs,把下面的代码一字不差地抄进去:
js
// ch01.mjs —— 第 1 章演示:手工更新的烦恼
// 小铃第一天送报纸。每份报纸赚 2 块钱。
let count = 1; // 送出的报纸份数
let money = count * 2; // 赚的钱 = 份数 × 2
console.log('一开始:送了 ' + count + ' 份,赚 ' + money + ' 元');
count = 2; // 又送出去一份!
// 糟糕!money 还是 2,它不知道 count 变了
console.log('忘了重新算:送了 ' + count + ' 份,赚 ' + money + ' 元');
money = count * 2; // 只好手动重新算一遍
console.log('手动重新算:送了 ' + count + ' 份,赚 ' + money + ' 元');第 5 步:运行它!
回到终端,输入:
bash
node demo/ch01.mjs🚀 现在跑一下,你应该看到:
一开始:送了 1 份,赚 2 元 忘了重新算:送了 2 份,赚 2 元 手动重新算:送了 2 份,赚 4 元第二行是不是很刺眼?送了 2 份却只赚 2 元——这就是忘了手动更新的后果!
🧰 TS 小课堂(预告版)
这一章我们用的是你熟悉的 JavaScript,所以没有新语法要学。
但从第 2 章开始,我们要换上一种更结实的语言:TypeScript(简称 TS)。它是 JavaScript 的大哥哥,多了一样东西——类型。类型就像是给每个消息贴上标签:"这是数字""这是文字",贴好标签,程序出错时能更早地大喊:"喂,这里不对!"
别担心,TS 一点也不吓人。遇到新语法时,书里会有专门的 TS 小课堂 给你讲明白。
📚 消息城词典
| 词 | 意思 | 记住它 |
|---|---|---|
| 信号 (signal) | 会变化的消息源 | 像小铃的报纸,一变,全城都知道 |
| 依赖 (dep) | 一条消息靠谁算出来 | money 依赖 count |
| 订阅者 (sub) | 在听某条消息的人 | 谁在看 count,谁就是它的订阅者 |
| 计算值 (computed) | 根据别的信号算出来的消息 | 第 4 章的主角 |
更多词会随着书慢慢长大,全部收录在附录·消息城词典。
📦 章末完整代码
这一章你只写了一个文件。来,对照检查一下你的目录结构是不是长这样:
signal-book/
└── demo/
└── ch01.mjsch01.mjs 的完整内容(和上面第 4 步抄的一模一样,抄全了就不用再看):
js
// ch01.mjs —— 第 1 章演示:手工更新的烦恼
// 小铃第一天送报纸。每份报纸赚 2 块钱。
let count = 1; // 送出的报纸份数
let money = count * 2; // 赚的钱 = 份数 × 2
console.log('一开始:送了 ' + count + ' 份,赚 ' + money + ' 元');
count = 2; // 又送出去一份!
// 糟糕!money 还是 2,它不知道 count 变了
console.log('忘了重新算:送了 ' + count + ' 份,赚 ' + money + ' 元');
money = count * 2; // 只好手动重新算一遍
console.log('手动重新算:送了 ' + count + ' 份,赚 ' + money + ' 元');✅ 跑通了? 只要
node demo/ch01.mjs能打出那三行,这一章就完成了!第 2 章我们会搭起正式的工作台。
🎮 动手试试
怎么玩
先合上答案,自己在本子上写出答案,再点开"答案"对照。忍住别偷看哦!
1. 预言输出(猜一猜)
下面这段代码运行后,会打印出什么?
js
let pages = 3;
let words = pages * 500;
console.log('一共 ' + words + ' 个字');
pages = 5;
console.log('一共 ' + words + ' 个字');👉 点开看答案
第一行打印 一共 1500 个字,第二行还是 一共 1500 个字!
因为 pages 改成 5 之后,words 没有重新算——它还是 3 × 500。只有再写一遍 words = pages * 500,它才会变成 2500。这正是这一章要解决的问题。
2. 帮小铃找 bug(抓错)
小铃写了一段代码,她想卖气球:每个气球 3 块钱。可运行结果不对,你能找出哪里错了吗?
js
let balloons = 4; // 气球个数
let price = balloons * 3; // 总价
balloons = 6; // 又吹了 2 个!
console.log('一共 ' + price + ' 元'); // 小铃期待 18 元👉 点开看答案
小铃忘了重新算 price。balloons 改成 6 之后,price 还是 4 × 3 = 12 元,所以会打印 一共 12 元。
改法:在打印前加一行 price = balloons * 3;。看,手工更新多容易忘!
3. 填空(补全)
下面这段代码少了一行。补上哪一行,才能让 score 在 level 变成 2 后变成 200?
js
let level = 1;
let score = level * 100;
level = 2;
// 这里少了一行!
console.log(score); // 希望打印 200👉 点开看答案
补上:score = level * 100;(放在 level = 2; 后面)。
4. 小挑战(想一想)
小铃的报纸每份赚 2 块钱。如果主编说"从明天起,每份赚 3 块钱",小铃的本子要改几处?
再想一想:你生活里还有没有"一变全变"的例子?(提示:电梯楼层、红绿灯、你手机上的通知小红点……)
👉 点开看答案
要改两处:money = count * 2 这一行要改成 count * 3;而且之后每次 count 变了,都要记得按 3 块钱重新算——这就是手工更新的麻烦:改一处容易,忘一处更容易。
至于"一变全变"的例子,答案没有标准,想到一个就是一个!比如:温度一变,暖气的开关自动调整;你发一条消息,所有人的对话框都冒出新消息。
🏁 本章小结
- 信号是会变化的消息源,消息城靠"一变全变"运转;
- 靠别的消息算出来的东西,叫依赖,关系画出来就是依赖图;
- 没有魔法的时候,改一个数要手动重算一大片,又累又容易忘——所以我们要造信号库。
下一章:搭工作台,准备开工!