FlowingDev

React 的魔术秀:组件是如何诞生的

揭秘 React 如何通过组件、状态和虚拟 DOM,将 JSX 这种 HTML 与 JavaScript 的“混血儿”,变身为可交互的 Web UI。

试用工具: React Playground

一句话概括

React 是一个用于构建用户界面的 JavaScript 库,它的核心思想是把 UI 拆分成一个个独立、可复用的代码块,也就是“组件”(components),当组件的数据变化时,界面会自动更新。

它解决了什么问题

想象一下古早时期(比如说 2010 年代初)的 Web 开发。你有个网页,想让它变得可交互。也许你希望一个“点赞”按钮在被点击后变成蓝色,并更新旁边的计数器。用 jQuery 这样的工具,你得这么写代码:“在页面上找到这个按钮,监听它的点击事件,当事件发生时,找到那个计数器元素并修改它的文本,然后再找到按钮图标并修改它的颜色。”

这是一种命令式(imperative)的编程方式。你是在给浏览器一份一步一步如何改变页面的“菜谱”。对于一个点赞按钮来说,这还行。但如果是一个完整的社交媒体信息流,有成百上千的帖子、评论、通知,还有一个实时聊天窗口,所有这些都在同时更新呢?这些“菜谱”就会变成一团乱麻的意大利面条式代码。一处改动可能会出乎意料地弄坏另一个完全不相关的地方。让 UI 与应用程序的数据保持一致,成了一场持续不断、错误百出的战斗。

Facebook 就曾大规模地经历过这场噩梦。2011 年,他们为了解决内部问题创造了 React。React 引入了一种声明式(declarative)模型。你不用再告诉浏览器如何一步步改变页面,而只需描述在任何给定的状态下,UI 应该长成什么样子。你只需要声明:“如果这篇帖子是 liked 状态,按钮就应该是蓝色的,计数是 101。如果不是 liked 状态,按钮就是灰色的,计数是 100。”就这么简单。当 liked 状态从 false 变为 true 时,React 会自己 figuring out 最高效的方式去更新按钮和计数器。它负责处理“如何做”,你只需要专注于“做什么”。

这种从命令式到声明式思维的转变,简直是一场革命。它让构建复杂、数据驱动的 UI 变得更可预测、可扩展,也更容易调试。

底层工作原理

React 的“魔法”其实不是魔法,而是三个核心思想的巧妙结合:JSX、带状态的组件(Components with State)以及虚拟 DOM(Virtual DOM)。

JSX:JavaScript 的语法糖

当你看到 React 代码时,你会发现一些看起来很像直接嵌入在 JavaScript 中的 HTML。这就是 JSX(JavaScript XML)。

const greeting = <h1>Hello, FlowingDev user!</h1>;

你的浏览器根本不知道这玩意儿是啥,因为它不是有效的 JavaScript。在代码运行之前,它需要被像 Babel 这样的工具“转译”(transform)。上面那行 JSX 会被转换成一个常规的 JavaScript 函数调用。

const greeting = React.createElement("h1", null, "Hello, FlowingDev user!");

每一个 JSX 标签都会变成一个 React.createElement() 调用。这个函数调用并不会创建一个真实的 HTML 元素。它创建的是一个轻量的 JavaScript 对象,用来描述你想要创建的元素——它的类型(h1)、它的属性(这里没有,所以是 null)以及它的子元素("Hello...")。这些对象就是虚拟 DOM 的基本构建块。

所以,JSX 只是“语法糖”。它让我们能用一种熟悉的、类似 HTML 的语法来描述我们的 UI,但在底层,我们实际上只是在创建一棵 JavaScript 对象树。

组件和 State:幕后主脑

组件就是一个返回一些 JSX 的 JavaScript 函数。它是一个可复用、自成一体的 UI 片段。

function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

这个 Greeting 组件很简单。它接受一个 name作为输入(称为 "prop"),然后渲染一个标题。但如果我们想让一个组件能记住事情并随时间变化,比如一个计数器呢?为此,我们需要“状态”(state)。

State 是一个组件“拥有”并且可以改变的数据。React 提供了一些特殊的函数,叫做 “Hooks”,来管理它。最常见的一个就是 useState。

import { useState } from 'react';

function Counter() {
  // "count" 就是我们的状态变量。
  // "setCount" 是我们能用来更新它的 *唯一* 函数。
  const [count, setCount] = useState(0); // 初始值是 0.

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

当你调用 setCount(count + 1) 时,你不仅仅是在改变一个变量。你是在告诉 React:“嘿,这个 Counter 组件的状态变了。请重新运行这个函数来获取新的 UI,然后更新屏幕。” 接着 React 会“重新渲染”(re-render)这个组件,获取带有更新后 count 的新 JSX,然后高效地更新网页。

虚拟 DOM:React 的秘密武器

直接修改真实的浏览器 DOM(即活生生的 HTML 元素树)是非常慢的。就像你只想移动一个相框,却把整个房间的墙都拆了重建一样。

React 用它的秘密武器——虚拟 DOM (VDOM)——来避免这种昂贵的操作。VDOM 只是保存在 JavaScript 内存中的一份 DOM 拷贝。它就是我们前面提到的那棵由 React.createElement 对象组成的树。

这个过程被称为协调(reconciliation):

  1. 初次渲染: 当 Counter 第一次渲染时,React 会构建一个 VDOM 树,描述了一个 div,里面包含一个文本为 "You clicked 0 times" 的 p 和一个 button。然后它用这个 VDOM 树来创建页面上真实的 DOM 元素。
  2. 状态变更: 你点击了按钮。setCount(1) 被调用。
  3. 重新渲染和新的 VDOM: React 重新运行 Counter 函数。它会构建一棵新的 VDOM 树。这棵新树和旧的几乎一模一样,但 p 元素现在描述的文本是 "You clicked 1 times"。
  4. 差异比较 (Diffing): React 的 “diffing” 算法会比较新旧两棵 VDOM 树。它会飞快地扫过它们并找出不同之处。在这个例子里,它只发现了一个区别:<p> 标签里的文本变了。
  5. 批量更新: React 把这一个微小的改动应用到真实的 DOM 上。它不会去碰 div 或 button;它只更新了 p 元素的文本。

这种 diff-and-patch(差异比对和修补)的过程快得令人难以置信。通过批量更新和最小化对缓慢的真实 DOM 的直接操作,即使在复杂数据快速变化时,React 也能让 UI 感觉非常流畅和响应迅速。

真实世界的案例

表单数据消失之谜

一个新手程序员接到了一个任务,要构建一个复杂的多步骤用户注册表单。每一步都是一个不同的组件。在某个环节,他需要在第二步里加一个小小的复选框,用来根据条件显示一个额外的输入框。他实现了这个功能,但突然出现了一个诡异的 bug:每当用户勾选那个复选框时,他们在第二步输入的所有数据都会消失得无影无踪。

罪魁祸首是对“key”的误解。为了显示那个额外的字段,这位开发者修改 JSX 结构的方式让 React 认为整个第二步的表单组件是一个全新的、不同的组件。由于没有一个稳定的 key prop 来标识它,React 销毁了旧的组件(以及它的所有 state,也就是用户输入的数据),然后在原地挂载了一个全新的组件。

教训: key 这个 prop 不仅仅是给列表用的。它是一个组件的稳定身份标识。理解 React 如何使用 key 在多次渲染之间追踪组件,对于保持 state 和避免意外重置至关重要。

无限 API 之锤

一个团队正在开发一个显示实时股票数据的仪表盘。一位开发者写了一个组件,用 useEffect hook 来获取特定股票的最新价格。代码大概是这样的:

useEffect(() => {
  fetchStockPrice(stockSymbol).then(price => {
    setPrice(price); // 用新价格更新 state
  });
}); // 糟糕,没有依赖项数组!

组件一加载,应用就卡死了,开发者笔记本的风扇开始狂转,网络监控显示每秒钟都有成千上万个相同的 API 请求被发出去。他们搞出了一个无限循环。useEffect 获取数据并调用 setPrice。state 更新导致组件重新渲染。因为 useEffect 没有依赖项数组,它在每一次渲染后都会再次运行,再次获取数据,再次更新 state,再次导致重新渲染,如此无限循环下去。

**教训:**像 useEffect 这样的 React Hook 是强大但精确的工具。你必须提供一个“依赖项数组”来告诉 React 何时重新运行这个 effect。如果省略它,就意味着“每次渲染后都运行”,这在与 state 更新结合时,简直是灾难的配方。

常见错误和陷阱

  • 直接修改 state。 绝对不要写 myState.prop = 'value' 或 myArray.push('item')。React 的变更检测依赖于对象和数组的引用身份。当你调用 setState 时,你必须提供一个新的对象或数组(例如,使用展开语法 ...),React 才能注意到变化。
  • 在列表中忘记写 key。 当你用 .map() 渲染一个项目列表时,你必须给每个元素一个唯一且稳定的 key prop(比如 item.id)。这是 React 的 diffing 算法能高效追踪哪些项被移动、添加或删除的方式。使用数组索引作为 key 是一个常见的反模式,可能会导致 bug。
  • 把 JSX 当成 HTML。 它很像,但不一样。你必须用 className 代替 class,用 htmlFor 代替 for,样式属性要用驼峰命名法(backgroundColor 而不是 background-color)。另外,所有标签都必须闭合,所以图片标签是 <img ... />,而不是 <img>。
  • 把 Hook 当成魔法。 像 useState 和 useEffect 这样的 Hook 遵循严格的规则(例如,它们只能在函数组件的顶层调用)。它们依赖于在多次渲染之间保持一致的调用顺序。试图在循环或条件语句中调用它们会搞垮你的应用。

为什么你应该关注它

React 不仅仅是一个库;它是一个生态系统,一种思维方式。它驱动了现代网络的一大部分,从 Instagram 和 Netflix 这样的大型网站,到无数的初创公司和内部工具。学习 React 意味着学习一种组件化的架构,它能促进代码的可复用性、可测试性和可维护性。

即使你不是每天都用 React,理解它的核心原理——声明式 UI、状态管理和 VDOM——也会让你成为一个更好的开发者。这些概念几乎影响了所有其他现代 UI 框架,从 Vue 到 Svelte。此外,有了 React Native,同样的技术和原理还可以用来为 iOS 和 Android 构建原生移动应用。对于今天任何从事用户界面开发的开发者来说,这都是一个必须掌握的强大范式。

深入了解

编辑器如何理解代码

语法颜色来自语言解析器,而不是程序执行。CodeMirror 和 Lezer 在浏览器本地识别符号和语法结构。建议结合关键字、受支持的局部名称、代码片段和内置的小型参考库;悬停在已收录的符号上可查看签名。建议并不保证运行时真的提供该 API。解析器在支持时检测错误语法,但不能替代类型检查或实际执行:语法正确的代码仍然可能失败。Ruby 和 Lua 使用基于词元的模式,诊断能力较有限。没有远程语言服务器,源代码无需离开设备。

CodeMirror & Lezer: MIT.

理论搞定,动手试试吧——100% 在你的浏览器中运行。

试用工具: React Playground