Skip to content

第 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 依赖 countcount 一变,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 demo

cd 是"走进文件夹",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.mjs

ch01.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 元
👉 点开看答案

小铃忘了重新算 priceballoons 改成 6 之后,price 还是 4 × 3 = 12 元,所以会打印 一共 12 元

改法:在打印前加一行 price = balloons * 3;。看,手工更新多容易忘!

3. 填空(补全)

下面这段代码少了一行。补上哪一行,才能让 scorelevel 变成 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 块钱重新算——这就是手工更新的麻烦:改一处容易,忘一处更容易

至于"一变全变"的例子,答案没有标准,想到一个就是一个!比如:温度一变,暖气的开关自动调整;你发一条消息,所有人的对话框都冒出新消息。

🏁 本章小结

  1. 信号是会变化的消息源,消息城靠"一变全变"运转;
  2. 靠别的消息算出来的东西,叫依赖,关系画出来就是依赖图
  3. 没有魔法的时候,改一个数要手动重算一大片,又累又容易忘——所以我们要造信号库

下一章:搭工作台,准备开工!