React 18 Hydration 深度解析:从同步阻塞到智能调度

原文:https://dev.to/heytechomaima/i-went-down-the-react-hydration-rabbit-hole-3292(作者 @heytechomaima)

好吧,我钻研了一番 React 的水合机制,并且我认为,我们通常讨论水合的方式,让它听起来比实际发生的事情简单得多。

在很多很多很多年里,每当谈论性能时,话题主要围绕网络展开:压缩你的 HTML。缓存你的资源文件。拆分你的代码包。发送更少的 JavaScript,不要让用户为了看一个落地页就下载半个互联网。这些建议都很好👍。但最终,JavaScript 还是要到达浏览器。然后呢?

浏览器依然需要解析、编译、执行它,而 React 也必须在主线程上完成自己的工作。这部分并不会因为你的包传输得快就消失。

而水合也并非某种神奇的轻量级操作,React 并不是看看 HTML 然后说“嗯,看起来不错👍”就完事了。

实际是有 CPU 工作在进行的。

浏览器已经有 DOM 了,因为服务器发送了 HTML。现在,React 必须介入,去理解现有的 DOM,构建其 Fiber 表示,将它期望的内容与已有的内容进行调和(Reconcile),附加必要的行为,并使那个服务器渲染的 UI 成为 React 能实际控制的东西。

所以当我开始阅读 React 18 的相关资料时,有趣的问题对我来说并不是“React 是否让水合变得更快了?”

更像是:

“等等……如果问题不仅在于我们水合得多快,还在于我们如何调度这项工作呢?”

这正是 React 18 的有趣之处。

首先,React 并不只是“附加事件监听器”

这听起来像是理所当然的事,直到我仔细研究。

我们常常随口就说:

React 水合了服务器渲染的 HTML。酷,但本质上,React 并不是像一个异常执着的实习生那样在 DOM 里到处溜达,给元素贴上 onClick 处理器。😂

服务器已经生成了 HTML。浏览器解析了这段 HTML 并创建了真实的 DOM 节点。然后,React 必须将其预期的组件结构与这些节点进行调和,并确定现有的 DOM 是否可以被复用。

_这就是 Fiber 变得重要的地方。_

在正常的客户端挂载(Mount)过程中,React 会在渲染 UI 的过程中创建宿主节点(Host Nodes)。而在水合过程中,这些节点已经存在。React 必须将其工作与浏览器已创建的内容进行匹配。

所以从概念上讲,如果服务器给了我们一个 <div>,而 React 期望在那个位置有一个 <div>,React 就可以认领并复用那个已有的节点,而不是把一切都当作全新的客户端渲染来处理。

这也是为什么水合不匹配(Hydration Mismatches)比“哎呀,React 打印了一个警告”更值得关注。

React 基本上是在尝试调和两个现实:

服务器已经放入 DOM 的内容

客户端 React 树所说的应该存在的内容。

如果这两个现实不一致,React 就会遇到问题。

你面对的不仅仅是控制台里难看的输出。你面对的是 React 无法建立它预期的 Fiber 树与现有 DOM 之间的关系。

那个小小的水合警告突然看起来就不那么无辜了。😭

然后我接触到了 Suspense,事情变得更加奇怪,因为我认为我们很多人学习 Suspense 时,理解基本上是:

某样东西正在加载 → 显示后备内容(Fallback)

这在技术上有用,但它完全隐藏了有趣的部分。

Suspense 不仅仅是你那个花哨的加载旋转器包装器。

它是围绕渲染工作的一个边界。

看看像这样的代码:

const Chart = lazy(() => import("./Chart"));

在那一刻,React 并不能立刻获得组件的实现。依赖项还在加载,而 React 有一种机制来处理这种未完成的工作:在渲染过程中,懒加载的组件可以挂起(Suspend)。React 随后会寻找最近的 Suspense 边界,并渲染后备内容,而不是假装组件已经准备就绪。

我觉得真正有趣的部分是,React 并没有“崩溃”。

从传统意义上说,什么都没出错。

渲染器基本上是遇到了它此刻无法完成的工作,然后说:

好吧,我没法继续处理这部分了。我将处理这个边界。

这与认为 Suspense 只是显示“正在加载...”的组件,是一种非常不同的心智模型。

后备内容是可见的部分,而边界才是有趣的部分。然后还有选择性水合(Selective Hydration)

从这里开始,React 18 的设计才真正有意义。

想象一下你的页面有一个评论区,而这个评论区位于一个 Suspense 边界之后。

页面是可见的。HTML 已经在那里了。但可能评论子树尚未完成水合。

然后用户过来,立刻点击了其中的某个东西。

这基本上是用户从你精心调度的渲染工作角度来看最烦人的一件事。😂

用户才不在乎 React 当前正忙着水合其他东西。

他们点了。

而这正是 React 18 调度模型的重要性所在。

React 不会将水合视为一个必须从上到下、在其他任何事情发生前完成的巨大检查清单。它可以优先处理与交互相关的工作。

那次点击很重要。

包含目标的 Suspense 边界可以成为更高优先级的水合目标。

React 可以致力于让那部分树准备就绪,一旦必要的 React 树可用,排队的事件就可以被重放。

我非常喜欢这个区分:

点击事件并未消失,只是React还没有准备好处理它。

这是两码事。

浏览器已经接收了事件。

React的任务是确定对应的React组件树何时准备好处理它。

一旦你不再把“并发React”想象成React同时处理五件事,这个细节就会让它听起来不再那么神秘。

它并非如此。

主线程依然存在。

你的CPU不会因为升级React就突然长出第二个大脑。😭

这自然把我们引向浏览器——这大概是我第二喜欢的部分。

浏览器不知道React的存在,它不关心Fiber架构,也不关心lane机制。

它不在乎你的node_modules深处是否有个调度器正在经历存在主义危机。

浏览器只认识JavaScript。

仅此而已。

React的调度器运行在JavaScript执行环境中。

所以当人们说React可以“中断渲染”时,别太抠字面意思。

React无法在函数执行中途随意冻结JavaScript代码,然后说:“稍等,用户点击了。”

JavaScript执行仍然发生在主线程上。

React能做的是将渲染工作组织成若干单元,并决定何时应该让出控制权给浏览器,而不是继续占用线程。

这个区别很重要:React并非在抢占JavaScript,而是通过让出控制权与浏览器协作。

因此心智模型可以转变为:

React不必同时渲染所有内容。

更确切地说是:

_React有很多工作要做,但不是所有工作都值得立即阻塞用户。_

我认为这才是并发React背后更有趣的理念:它并非让CPU神奇地变快,而是让React的工作变得可中断、可优先排序、可调度。

这比“React 18加速了水合过程”这个概念更有分量。

因为CPU的工作量仍然存在。

JavaScript代码依然存在。

水合过程依然存在。

React只是更擅长判断:“好的,这个可以等等。用户刚刚执行了操作。”对于UI库而言,这是个相当重要的区别。

越是深入研究React内部机制,我就越意识到开发者日常使用的很多功能背后,都隐藏着一些非常有趣的工程设计。

我还在不断学习中,所以如果这里有任何理解偏差,请务必告诉我!我真心希望得到纠正,而不是自信满满地发布错误内容。😭

很想知道大家对水合与并发React的看法,或者最近掉进了什么React内部机制的兔子洞。

欢迎在下方分享你们的见解

原文:https://dev.to/heytechomaima/i-went-down-the-react-hydration-rabbit-hole-3292(作者 @heytechomaima)

发布评论
全部评论(0)