跳至正文
来两杯美式
返回

React 核心(2):声明式与 React 的实现机制

By 来两杯美式
发布于更新于

一、两种做事方式

编程里所有代码都能归为两种风格:

命令式(Imperative)声明式(Declarative)
你告诉电脑怎么做(一步步指令)要什么(描述结果)
思维方式“先改这个,再改那个”“数据长这样,页面就该长这样”
你的职责数据 + UI 更新全管只管数据,UI 自动跟着变

生活类比:点菜

命令式点菜

把锅烧热 → 倒油 → 打两个鸡蛋 → 加盐翻炒 → 装盘给我

你在指挥每一个步骤。

声明式点菜

我要一份番茄炒蛋

你只说了”我要什么”,厨师自己知道怎么做。

代码对比:最直白的

同样是”点击按钮,数字 +1”:

// 命令式:你告诉浏览器每一步做什么
let count = 0;
const span = document.getElementById("count");
const btn = document.getElementById("btn");

btn.addEventListener("click", () => {
  count = count + 1; // ① 手动改数据
  span.textContent = count; // ② 手动更新 DOM
});
// 声明式:你只描述"UI 长什么样"
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

区别在于:命令式你管数据 DOM 两件事;声明式你只管数据,DOM 由框架替你更新。

二、声明式不是魔法——必须有人替你干命令式的脏活

声明式的本质是”你只管描述结果,有人替你执行过程”。但”有人”不是凭空出现的——React 造了三层基础设施来当这个”有人”:

第一层:Virtual DOM —— 让你能”描述”而不是”操作”

// 没有 Virtual DOM:必须操作真实 DOM(命令式)
document.createElement("div");
element.appendChild(child);
element.setAttribute("class", "active");
element.removeChild(oldChild);

// 有 Virtual DOM:只需要"描述"UI 长什么样(声明式)
return (
  <div className="active">
    <Child />
  </div>
);

Virtual DOM 是一棵 JS 对象树,描述”UI 应该长什么样”。你写 JSX → React 把它变成 JS 对象 → 这个对象跟真实 DOM 没关系,它只是一份设计图纸

去掉这层会怎样?你必须直接操作真实 DOM,那就不可能声明式了。

第二层:Reconciliation(协调 / Diff 算法)—— 让 React 知道”怎么变”

你声明了新的 UI 状态,但浏览器只认真实 DOM。React 必须搞清楚:旧图纸和旧 DOM 之间,哪些地方变了?

旧 Virtual DOM:  <div><span>A</span><span>B</span></div>
新 Virtual DOM:  <div><span>A</span><span>C</span></div>

Diff 算法发现:第二个 <span> 的内容从 B 变成了 C
→ 只更新这一个真实 DOM 节点,其他不动

Diff 算法做的事:对比新旧两棵 Virtual DOM 树,算出最小的真实 DOM 操作,然后执行。

去掉这层会怎样?每次状态变化就得整体替换 DOM,性能不可接受,声明式就没人用了。

第三层:状态驱动渲染 —— 让”变化”自动发生

光有图纸和 Diff 还不够,还得有一个触发机制:什么时候该重新画图纸?

const [count, setCount] = useState(0);
//  ↑ 状态         ↑ 修改状态的唯一方式

return <button onClick={() => setCount(count + 1)}>{count}</button>;

完整的链条:

用户点击按钮
  → 调用 setCount(1)
    → React 知道状态变了
      → 重新执行组件函数,拿到新的 Virtual DOM
        → Diff 对比新旧 Virtual DOM
          → 只更新真实 DOM 中变化的部分

useState 保证了状态是唯一的真相来源,UI 永远是状态的映射。你不需要手动同步——改了状态,UI 自动跟着变。

去掉这层会怎样?改了数据还得手动调用更新函数,又回到命令式了。

三层缺一不可

解决什么问题去掉会怎样
Virtual DOM用 JS 对象描述 UI,不用碰真实 DOM必须手写 DOM 操作,无法声明式
Diff 算法高效算出”哪些真实 DOM 该改”每次全量重建 DOM,性能不可接受
状态驱动渲染状态变了自动触发重新渲染改了数据还得手动刷新 UI,回到命令式

三、实战:useQuery 的声明式数据获取

理解了 React 的声明式机制,再看 TanStack Query 入门指南 里的这段代码:

const { data, isPending } = useQuery({
  queryKey: ["todos"],
  queryFn: getTodos,
});

if (isPending) return <Spinner />;
return <List data={data} />;

这是声明式的——你在说”数据没到时显示 Spinner,到了就显示 List”,没有写什么时候发请求、请求回来后怎么更新 UI、DOM 怎么从 Spinner 换成 List。

useQuery 怎么让 isPending 变化并触发重渲染

你写的代码                useQuery 内部                   React
─────────               ──────────────                  ─────

useQuery({queryKey...})

  ├─ 创建 QueryObserver
  │    └─ 订阅 QueryCache 中的 Query 对象

  ├─ 返回 { isPending: true, data: undefined }

  │                      发起 fetch 请求...
  │                      ...等待...
  │                      请求成功!Query 内部状态更新
  │                           │
  │                           ▼
  │                      Query 通知所有 Observer:
  │                      "我的状态变了"
  │                           │
  │                           ▼
  │                      Observer 调用 React 的 setState()
  │                           │
  ◄───────────────────────────┘

  │  React 检测到 setState → 触发组件重新渲染

  ├─ useQuery 再次执行
  │   从 Observer 读最新状态
  │   返回 { isPending: false, data: [...] }

  └─ 组件用新返回值渲染 <List />

关键角色是 QueryObserver——TanStack Query 和 React 之间的桥梁:

这和你自己写 const [x, setX] = useState(0) 然后 setX(1) 触发重渲染是完全一样的机制——useQuery 只是把这个 setState 藏在了 Observer 内部。

类比:等快递

角色现实代码
等快递,不用一直盯着门口组件渲染,等 useQuery 返回值
快递员送到后按门铃异步操作完成,Query 更新状态
门铃通知你”东西到了”Observer 调用 setState
你去取听到门铃去门口拿React 重渲染,useQuery 返回新值

你不用每隔 5 分钟跑去门口看快递到了没(轮询 / 命令式),门铃响了你就知道(事件通知 / 声明式)。

四、声明式在 React 中的三种体现

维度命令式做法React 声明式做法
UI 渲染手动创建 / 更新 / 删除 DOM 节点JSX 描述 UI = f(state),状态变则 UI 自动变
数据获取手动调 fetch → 手动判断 loading → 手动渲染useQuery 返回值随状态自动变,你只写”每种状态怎么展示”
异步等待每个组件自己管 isLoading 判断Suspense:组件只管渲染,loading 统一交给外层 <Suspense>

从左到右,声明式的程度越来越高——你管的”过程”越来越少,框架管的越来越多。Suspense 是目前 React 生态里声明式程度最高的方案:组件里连 isPending 都不用判断了。

五、一句话总结

声明式 = 你只管描述结果,有人替你执行过程。 React 花了三层基础设施来当这个”有人”——Virtual DOM 是语言(让你能描述),Diff 是大脑(算出怎么变),状态驱动是心跳(驱动变化发生)。


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
React
第 2 / 17 篇
查看系列全部文章
  1. 01.React 核心(1):React 组件与函数的本质区别
  2. 02.React 核心(2):声明式与 React 的实现机制
  3. 03.React 核心(3):Props 完全指南
  4. 04.React 核心(4):useState 完全指南
  5. 05.React 核心(5):useEffect 完全指南
  6. 06.React 核心(6):useRef 完全指南
  7. 07.React 核心(7):Context API 与状态共享
  8. 08.React 核心(8):useCallback 与 useMemo
  9. 09.React 核心(9):Hooks 规则与闭包陷阱
  10. 10.React 核心(10):自定义 Hook 设计
  11. 11.React 核心(11):Key 与列表渲染
  12. 12.React 核心(12):表单与受控/非受控组件
  13. 13.React 核心(13):React 渲染机制深入——Fiber 与调度
  14. 14.React 核心(14):React 18 并发特性
  15. 15.React 核心(15):Suspense 机制详解
  16. 16.React 核心(16):TanStack Query 入门指南
  17. 17.React 核心(17):TanStack Query 缓存机制详解

上一篇
React 核心(3):Props 完全指南
下一篇
React 核心(1):React 组件与函数的本质区别