跳到主要内容

React diff 算法源码拆解:子节点调和的四大步骤

· 阅读需 12 分钟

上一篇我们讲了 React 19 把属性级 diff(prepareUpdate/updatePayload)挪进了 commit 阶段。但大家常说的"React diff 算法",其实指的是另一个完全不同的东西——子节点调和(reconciliation):比较同一层级的 children,决定哪些 fiber 复用、哪些删除、哪些新建、哪些移动。

本文从源码角度拆解这个算法,核心是 reconcileChildrenArray 的四大步骤(这四步直接写在 React 源码 ReactChildFiber.js 的注释里)。先概述全局,再逐步骤分论。


OpenAI Codex 三大开源组件:exec、SDK、app-server 用法全解

· 阅读需 12 分钟

很多人以为"强大的 AI Agent = 好模型 + 好 Prompt"。但实际上,模型之上还叠着一整套底层执行系统(Harness):理解任务、维护对话记忆、调用工具、实时展示进度、处理失败、请求人类审批。这部分决定了 Agent 能不能真正"干活",也决定了把 Agent 嵌进产品时的体验上限。

OpenAI 在开源仓库 openai/codex 里把这套 harness 的三个组件开放了出来,分层恰好对应三种不同的接入方式:

组件形态一句话适合场景
codex execCLI 子命令非交互式跑完一个任务脚本、CI、一次性后台任务
Codex SDKTypeScript / Python 库在应用代码里启动/恢复/流式监听任务CI/CD、内部工具、应用集成
Codex app-server本地 JSON-RPC 服务把 Agent 变成你产品的"一等公民"持久对话、实时事件、打断、自定义工具、审批

下面逐个过用法,代码片段可以直接跑。

用 D2 画流程图:另一种「文本即图表」的打开方式

· 阅读需 3 分钟

Mermaid 大家都很熟了,但「文本即图表」还有另一个后起之秀——D2(d2lang.com),一个用 Go 写的声明式图表语言。它把布局交给引擎(dagre / ELK)自动排版,写图就像写数据一样:节点、连线、容器,全部是声明。

这篇博客本身就在用 D2 画图——下面的流程图和应用架构图都是 ```d2 代码块在构建时编译成的 SVG。你可以直接看效果,再决定要不要入坑。

把性能指标和转化率绑在一起:一个实习生终结"开发运营甩锅"的监测系统

· 阅读需 10 分钟

先讲一个传闻(真假不重要,思路值得抄):据说某大厂有个实习生,在周会上被夹在中间——运营指着大盘说"转化率跌了 5%,是不是你们前端最近改版改坏的?",开发指着另一块屏说"LCP 1.8 秒,性能数据没退化啊,是不是你们投放渠道变了?"。两边各拿一套指标,谁都无法证明自己的清白,也谁都无法证明对方的锅。

那个实习生的做法很朴素:他花了几周,把"性能指标"和"业务指标"绑进了同一张表、同一个会话、同一块大盘——从此"性能到底影不影响业务"不再靠吵,靠查。

这篇文章聊聊这套思路:为什么性能真的影响转化(有数据)、怎么把两者"绑"起来、系统的架构长什么样,以及——怎么用数据终止甩锅,而不是制造新的甩锅。


TanStack Query 实战:一个「收藏星标」的乐观更新

· 阅读需 13 分钟

收藏功能几乎是每个内容产品的标配,但它恰恰是「手写 fetch 最容易写脏」的一类交互:点一下,UI 要立刻翻转,网络请求在后台跑,失败还得翻回去。这背后其实藏着一串问题——loading 怎么禁按钮?等网络还是先反馈?失败怎么恢复?详情页、列表页、收藏页三处的数据怎么同步?

这篇文章用项目里一个真实的 FavoriteStar 组件,讲清楚 TanStack Query 是怎么把这四件事全部接管的。你会发现:这些逻辑一行都没手写,全靠 useMutation 的生命周期钩子。

泛域名 HTTPS 解析实践:一张通配证书,Nginx 按 host 分发所有子站点

· 阅读需 7 分钟

一台服务器上跑着博客、网盘、GitLab、漫画站……每个都想有自己的子域名(blog.、git.、nas.),每个子域名都想走 HTTPS。最省事的做法是:一张泛域名证书 + 一个泛域名 server 块,接收所有子域名,再按 host 分发到各自的服务。


1. 场景:一个域名,N 个子服务​

需求是这样的:

两种实现对比:

方案证书数量server 块新增子域名
每域名单独签N 张N 个要重新签证书 + 加配置
泛域名证书1 张1 个泛块 + 需要时加精确块DNS 加记录 + map 加一行

泛域名方案的核心收益:证书只管一次,新子域名零证书成本。

React 19:把属性 diff 从渲染阶段挪进 commit 阶段 —— prepareUpdate/updatePayload 的终结

· 阅读需 8 分钟

很多 React 教程(尤其是写 React 16–18 的书)里会有这样一段:"HostComponent 在 completeWork 的更新流程中,属性发生变化时,会把 diff 的结果以 ['title', 1, 'style', {'color': '#333'}] 这样的扁平数组保存在 updateQueue 里。"

这段描述是真的,但只对 React 16–18 成立。React 19 干了一件大事:把 prepareUpdate 整个删掉,属性 diff 从"渲染阶段预计算"挪到了"commit 阶段边 diff 边应用"。这个改动同时回答了一个经典困惑——updateQueue 到底是"环形链表"还是"key-value 数组"?答案是:它俩都对,因为 updateQueue 是"多态"的,而这个改动恰好把其中一个形态清掉了。

本文扒开这个改动的全部细节:前后机制、diffProperties 怎么生成数组、为什么要删(PR #26583)。


React 19 弃用 forwardRef:ref 传参方式的前后对比

· 阅读需 5 分钟

在 React 里,「把一个 DOM 节点的引用从父组件传给子组件」——这么简单一件事,React 18 及之前却得包一层 forwardRef:

import { forwardRef, useRef } from 'react';

const FancyInput = forwardRef(function FancyInput(props, ref) {
return <input ref={ref} {...props} />;
});

function App() {
const inputRef = useRef<HTMLInputElement>(null);
return <FancyInput ref={inputRef} />;
}

为什么不能直接写 function FancyInput(props) 然后取 props.ref?因为在 React 19 之前,ref 不是普通 prop——它是 React 内部的特殊属性:类组件通过它拿到实例引用,而函数组件默认什么都收不到,只能靠 forwardRef 显式接收再转发。

React 19 把这件事彻底改掉了:ref 变成了普通 prop,forwardRef 被官方标记为 deprecated(弃用,但兼容可用)。新代码不再需要它。

大厂自建沙箱实现微前端:从 Proxy 沙箱到 iframe 沙箱的完整拆解

· 阅读需 13 分钟

微前端把多个团队的应用塞进同一个页面,但浏览器只给了一个 window、一个 document、一条渲染管道。两个子应用如果都往全局上写东西、都动全局样式、都监听全局事件,必然互相踩脚。所以微前端框架的核心难点从来不是"怎么把代码加载进来",而是怎么给每个子应用一个互不干扰的"沙箱"。

本文拆解各大厂自建沙箱的实现:先讲沙箱的本质(JS 全局隔离有哪几条路),再逐家拆 qiankun(蚂蚁)、micro-app(京东)、wujie(腾讯)、Garfish(字节)的方案与权衡,最后给出一套自建沙箱的工程要点。


useEffectEvent:把"非响应式逻辑"从 Effect 里抽出来

· 阅读需 20 分钟

在 React 里,你迟早会遇到这么个两头堵的问题:Effect 里要读某个最新的 props / state,但又不想它一变就让 Effect 重跑。加进依赖数组吧,副作用会过度执行(反复重连、反复重置定时器);不加吧,闭包读到的永远是"过期的旧值",而且 ESLint 还会红字报警。

React 19.2(2025 年 10 月转正)带来了专门解决这个问题的 useEffectEvent。但要真正理解它为什么长这样、限制为什么那么多,得从 React 最底层的心智模型——不可变(Immutable)快照——说起。


1. 从 Immutable 说起:每次渲染都是一份"照片"​

React 的 props 和 state 是不可变的。这句话的意思是:更新 state 不是"把旧值改掉",而是丢弃旧快照、生成一个新快照、然后带着新快照重新渲染一次。

这个模型带来一个极其关键的推论:组件体里的每一个闭包,捕获的都是"这一次渲染"的快照值。

function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setTimeout(() => {
console.log(count); // 打印的是"创建 effect 时"的 count,不是最新的
}, 1000);
return () => clearTimeout(id);
}, []);

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

上面这个例子里,不管点多少次按钮,setTimeout 回调里打印的 count 都是第一次渲染时的值(0)。因为 useEffect 的回调是在"渲染 #1"里创建的,它捕获的 count 就是渲染 #1 的快照——之后 state 变了,组件重新渲染,但旧闭包不会"自动看到"新值。

对比 Vue 的响应式模型会更容易看清这个差异:Vue 的 ref / reactive 是可变的代理,effect 里读 .value 永远是"运行时的当前值"(依赖收集 + 追踪);而 React 的值在渲染那一刻就是定死的,不存在"活的引用"这种东西。

所以 React 里"在 effect 里读最新值"天然是个难题——这就是 useEffectEvent 存在的全部理由。