链式反应 - A Chain Reaction(翻译)

原文链接

A Chain Reaction

中文翻译

我在编辑器中写下了一些JSX代码:

1
2
3
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Alice</i>!
</p>

目前,这段信息只存在于我的设备上。但凭借一些运气,它将穿越时空到达你的设备,并呈现在你的屏幕上。

图片

这一功能的实现是工程学的一个奇迹。在你的浏览器内部,有代码片段知道如何显示一个段落或者以斜体绘制文本。这些代码片段在不同的浏览器之间是不同的,甚至在同一浏览器的不同版本之间也有所不同。在不同操作系统上,屏幕的渲染方式也各不相同。

然而,因为这些概念已经有了公认的名称(例如<p>代表段落,<i>代表斜体),我可以不担心它们在你的设备上如何真正工作而引用它们。我不能直接访问它们的内部逻辑,但我知道我可以传递哪些信息给它们(例如CSS的className)。多亏了网络标准,我可以合理地确信我的问候将按我的意图显示。

<p><i> 这样的标签让我们能够引用浏览器内置的概念。然而,名称并不一定非得引用内置的东西。例如,我正在使用 text-2xlfont-sans 这样的CSS类来美化我的问候语。这些名称并不是我自己想出来的——它们来自一个名为Tailwind的CSS库。我已经在本页面中包含了这个库,这让我可以使用它定义的任何CSS类名。

接下来,文章讨论了为什么我们喜欢给事物命名,以及如何在浏览器中呈现自定义的JSX组件。这包括了如何定义一个Greeting组件,以及如何将这个组件“翻译”为浏览器能理解的HTML标记。

为什么我们喜欢给事物命名?

我写下了 <p><i>,我的编辑器识别了这些名称。你的浏览器也这样做。如果你做过一些网络开发,你可能也认出了它们,甚至可能通过阅读标记猜到了屏幕上会出现什么。从这个意义上说,名称帮助我们以一些共享的理解开始。

从根本上讲,计算机执行相对基础的指令——比如加法或乘法运算、在内存中写入和读取数据,或者与显示设备等外部设备通信。仅仅在屏幕上显示一个 <p> 可能涉及执行数十万条这样的指令。

如果你看到了计算机为了在屏幕上显示一个 <p> 而执行的所有指令,你几乎无法猜测它们在做什么。这就像试图通过分析房间里四处弹跳的所有原子来判断正在播放哪首歌。这看起来似乎是不可理解的!你需要“拉远镜头”来看清发生了什么。

要描述一个复杂系统,或者指导一个复杂系统做什么,将行为分解为建立在彼此概念之上的层次是有帮助的。

这样,从事屏幕驱动程序工作的人员可以专注于如何将正确的颜色发送到正确的像素。然后,从事文本渲染工作的人员可以专注于每个字符应如何变成一堆像素。这让我这样的人可以专注于为我的“段落”和“斜体”选择恰到好处的颜色。

我们喜欢名称,因为它们让我们忘记了它们背后的东西。

我已经使用了许多其他人想出的名字。有些是浏览器内置的,比如 <p><i>。有些则内置在我使用的工具中,比如 text-2xlfont-sans。这些可能是我的构建模块,但我到底在建造什么呢?

例如,这是什么?

1
2
3
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Alice</i>!
</p>

图片

从浏览器的角度来看,这是一个带有特定CSS类(使其变大和变紫)的段落,里面还有一些文本(部分是斜体)。

但从我的角度来看,这是对Alice的问候。尽管我的问候碰巧是一个段落,但大多数时候我更愿意这样考虑:

1
<Greeting person={alice} />

给这个概念一个名字为我提供了一些新发现的灵活性。现在我可以显示多个问候,而无需复制和粘贴它们的标记。我可以向它们传递不同的数据。如果我想改变所有问候的外观和行为,我可以在一个单独的地方做到。将问候变成它自己的概念让我可以分别调整“显示哪些问候”和“问候是什么”。

然而,我也引入了一个问题。

现在我已经给这个概念命了名,我心中的“语言”与你的浏览器所说的“语言”不同。你的浏览器知道 <p><i>,但它从未听说过 <Greeting> ——那是我自己的概念。如果你想让你的浏览器理解我的意思,我就得“翻译”这段标记,只使用浏览器已经知道的概念。

我需要将这个:

1
<Greeting person={alice} />

转换成这个:

1
2
3
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Alice</i>!
</p>

我该如何着手呢?

要命名某物,我需要定义它。

例如,在我没有定义 alice 之前,它没有任何意义:

1
2
3
4
const alice = {
firstName: 'Alice',
birthYear: 1970
};

现在 alice 指代那个 JavaScript 对象。

同样,我需要实际定义我的 Greeting 概念的含义。

我将为任何人定义一个 Greeting,作为一个段落,显示“Hello, ”后面跟着那个人的名字的首字母大写,再加上一个感叹号:

1
2
3
4
5
6
7
function Greeting({ person }) {
return (
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>{person.firstName}</i>!
</p>
);
}

alice 不同,我将 Greeting 定义为一个函数。这是因为每个人的问候都应该是不同的。Greeting 是一段代码——它执行一个转换或翻译。它将一些数据转换成一些用户界面。

这让我知道该如何处理这个:

1
<Greeting person={alice} />

你的浏览器不会知道什么是 Greeting ——那是我自己的构想。但现在我为那个概念写了定义,我可以应用这个定义来“解开”我的意思。你看,对一个人的问候实际上就是一个段落:

1
2
3
4
5
6
7
function Greeting({ person }) {
return (
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>{person.firstName}</i>!
</p>
);
}

alice 的数据插入到那个定义中,我最终得到这个最终的 JSX:

1
2
3
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Alice</i>!
</p>

此时,我只引用了浏览器自己的概念。通过将 Greeting 替换为我所定义的它,我已经为你的浏览器“翻译”了它。

图片

现在,让我们教计算机做同样的事情。

看看JSX是由什么构成的。

1
2
3
const originalJSX = <Greeting person={alice} />;
console.log(originalJSX.type); // Greeting
console.log(originalJSX.props); // { person: { firstName: 'Alice', birthYear: 1970 } }

在幕后,JSX构建了一个对象,其中type属性对应于标签,props属性对应于JSX属性。

你可以将type看作是“代码”,props看作是“数据”。要得到结果,你需要像我之前做的那样将数据插入到代码中。

我写了一个小函数,它做到了这一点:

1
2
3
4
function translateForBrowser(originalJSX) {
const { type, props } = originalJSX;
return type(props);
}

在这种情况下,type将是Greeting,props将是{ person: alice },所以translateForBrowser(<Greeting person={alice} />)将返回调用Greeting并以{ person: alice }作为参数的结果。

这正如你从上一节可能记得的,将给我这个:

1
2
3
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Alice</i>!
</p>

这正是我想要的!

你可以验证,将我的原始JSX输入到translateForBrowser将产生只引用<p><i>等概念的“浏览器JSX”。

1
2
3
4
5
6
7
const originalJSX = <Greeting person={alice} />;
console.log(originalJSX.type); // Greeting
console.log(originalJSX.props); // { person: { firstName: 'Alice', birthYear: 1970 } }

const browserJSX = translateForBrowser(originalJSX);
console.log(browserJSX.type); // 'p'
console.log(browserJSX.props); // { className: 'text-2xl font-sans text-purple-400 dark:text-purple-500', children: ['Hello', { type: 'i', props: { children: 'Alice' } }, '!'] }

我可以对“浏览器JSX”做很多事情。例如,我可以将其转换为要发送到浏览器的HTML字符串。我也可以将其转换为更新已存在DOM节点的指令序列。现在,我不会专注于使用它的不同方式。现在重要的是,当我拥有“浏览器JSX”时,就没有什么可“翻译”的了。

就好像我的<Greeting>已经溶解了,<p><i>是残留物。

让我们尝试一些稍微复杂的东西。假设我想将我的问候包装在<details>标签中,使其默认折叠显示:

1
2
3
<details>
<Greeting person={alice} />
</details>

浏览器应该这样显示它(点击“详情”以展开它!)

现在,我的任务是弄清楚如何将这个:

1
2
3
<details>
<Greeting person={alice} />
</details>

转换成这个:

1
2
3
4
5
<details>
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Alice</i>!
</p>
</details>

让我们看看translateForBrowser是否已经可以处理这个。

1
2
3
4
5
6
7
const originalJSX = (
<details>
<Greeting person={alice} />
</details>
);
console.log(originalJSX.type); // 'details'
console.log(originalJSX.props); // { children: { type: Greeting, props: { person: alice } } }

你将在translateForBrowser调用中得到一个错误:

1
2
3
4
function translateForBrowser(originalJSX) {
const { type, props } = originalJSX;
return type(props); // 🔴 TypeError: type is not a function
}

这里发生了什么?我的translateForBrowser实现假设type,即originalJSX.type,总是像Greeting这样的函数。

然而,注意这次originalJSX.type实际上是一个字符串:

1
2
3
4
5
6
7
const originalJSX = (
<details>
<Greeting person={alice} />
</details>
);
console.log(originalJSX.type); // 'details'
console.log(originalJSX.props); // { children: { type: Greeting, props: { person: alice } } }

当你用小写字母开始一个JSX标签(比如<details>),根据约定,它被假定为你想要一个内置标签,而不是你定义的某个函数。

由于内置标签没有任何与它们相关联的代码(那个代码在浏览器内部的某个地方!),type将是一个像’details’这样的字符串。<details>的工作原理对我的代码是不透明的——我真正知道的只有它的名字。

让我们将逻辑分为两种情况,现在先跳过内置标签的翻译:

1
2
3
4
5
6
7
8
function translateForBrowser(originalJSX) {
const { type, props } = originalJSX;
if (typeof type === 'function') {
return type(props);
} else if (typeof type === 'string') {
return originalJSX;
}
}

这样改变后,translateForBrowser只有在原始JSX的type实际上是像Greeting这样的函数时才会尝试调用一些函数。

这就是我想要的结果,对吧?

1
2
3
<details>
<Greeting person={alice} />
</details>

等等。我想要的是这个:

1
2
3
4
5
<details>
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Alice</i>!
</p>
</details>

在我的翻译过程中,我想跳过<details>标签,因为它的实现对我是不透明的。我不能用它做任何有用的事情——这完全取决于浏览器。然而,它里面的任何东西可能仍然需要被翻译!

让我们修复translateForBrowser,以翻译任何内置标签的子元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function translateForBrowser(originalJSX) {
const { type, props } = originalJSX;
if (typeof type === 'function') {
return type(props);
} else if (typeof type === 'string') {
return {
type,
props: {
...props,
children: translateForBrowser(props.children)
}
};
}
}

这样改变后,当它遇到像<details>...</details>这样的元素时,它将返回另一个<details>...</details>标签,但里面的东西将再次用我的函数翻译——这样Greeting就会消失:

1
2
3
4
5
<details>
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Alice</i>!
</p>
</details>

现在,我再次说起了浏览器的“语言”:

详情

Greeting已经溶解了。

现在假设我尝试定义一个ExpandableGreeting

1
2
3
4
5
6
7
function ExpandableGreeting({ person }) {
return (
<details>
<Greeting person={person} />
</details>
);
}

这是我新的原始JSX:

1
<ExpandableGreeting person={alice} />

如果我将它通过translateForBrowser处理,我会得到这样的JSX作为返回:

1
2
3
<details>
<Greeting person={alice} />
</details>

但这不是我想要的!它仍然有一个Greeting在里面,我们不认为一段JSX是“浏览器就绪”,直到我所有的自定义概念都消失了。

这是我的translateForBrowser函数中的一个错误。当它调用一个像ExpandableGreeting这样的函数时,它将返回它的输出,而没有做其他任何事情。但我们需要继续进行!返回的JSX也需要被翻译。

幸运的是,有一个简单的方法可以解决这个问题。当我调用一个像ExpandableGreeting这样的函数时,我可以取回它返回的JSX,然后接下来翻译它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function translateForBrowser(originalJSX) {
const { type, props } = originalJSX;
if (typeof type === 'function') {
const returnedJSX = type(props);
return translateForBrowser(returnedJSX);
} else if (typeof type === 'string') {
return {
type,
props: {
...props,
children: translateForBrowser(props.children)
}
};
}
}

我还需要在没有东西可以翻译时停止这个过程,比如如果它接收到null或者一个字符串。如果它接收到一个事物的数组,我需要翻译每一个。有了这两个修复,translateForBrowser就完成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function translateForBrowser(originalJSX) {
if (originalJSX == null || typeof originalJSX !== 'object') {
return originalJSX;
}
if (Array.isArray(originalJSX)) {
return originalJSX.map(translateForBrowser);
}
const { type, props } = originalJSX;
if (typeof type === 'function') {
const returnedJSX = type(props);
return translateForBrowser(returnedJSX);
} else if (typeof type === 'string') {
return {
type,
props: {
...props,
children: translateForBrowser(props.children)
}
};
}
}

现在,假设我从这个开始:

1
<ExpandableGreeting person={alice} />

它将变成这个:

1
2
3
<details>
<Greeting person={alice} />
</details>

这将变成这个:

1
2
3
4
5
<details>
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Alice</i>!
</p>
</details>

在那一点上,过程将停止。

让我们再次看看这是如何工作的,这次带一些额外的深度。

我将这样定义WelcomePage

1
2
3
4
5
6
7
8
9
10
function WelcomePage() {
return (
<section>
<h1 className="text-3xl font-sans pb-2">Welcome</h1>
<ExpandableGreeting person={alice} />
<ExpandableGreeting person={bob} />
<ExpandableGreeting person={crystal} />
</section>
);
}

现在假设我以这个原始JSX开始:

1
<WelcomePage />

你能在你脑海中追溯转换的序列吗?

让我们一起一步步来做。

首先,想象WelcomePage溶解,留下它的输出:

1
2
3
4
5
6
<section>
<h1 className="text-3xl font-sans pb-2">Welcome</h1>
<ExpandableGreeting person={alice} />
<ExpandableGreeting person={bob} />
<ExpandableGreeting person={crystal} />
</section>

然后想象每个ExpandableGreeting溶解,留下它的输出:

1
2
3
4
5
6
7
8
9
10
11
12
<section>
<h1 className="text-3xl font-sans pb-2">Welcome</h1>
<details>
<Greeting person={alice} />
</details>
<details>
<Greeting person={bob} />
</details>
<details>
<Greeting person={crystal} />
</details>
</section>

然后想象每个Greeting溶解,留下它的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<section>
<h1 className="text-3xl font-sans pb-2">Welcome</h1>
<details>
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Alice</i>!
</p>
</details>
<details>
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Bob</i>!
</p>
</details>
<details>
<p className="text-2xl font-sans text-purple-400 dark:text-purple-500">
Hello, <i>Crystal</i>!
</p>
</details>
</section>

现在没有什么可以“翻译”的了。我所有的概念都已经溶解。

欢迎

详情

详情

详情

这感觉就像是一个链式反应。你混合一些数据和代码,它持续转化,直到没有更多的代码可以运行,只剩下残留物。

如果有一个库能为我们做这些就好了。

但等等,这里有一个问题。这些转换必须在你的电脑和我的电脑之间的某个地方发生。那么它们在哪里发生呢?

是在您的电脑上发生的吗?

还是在我的电脑上发生的?