React 16中,hydrate()和render()有何区别?

127

我已经阅读了文档,但我并不真正理解 React 16 中 hydrate()render() 的区别。

我知道 hydrate() 用于结合服务器端渲染和客户端渲染。

有人能解释一下什么是hydration,并且ReactDOM中的区别是什么吗?


以下由@tophar给出的答案是正确的,如果你想更深入地探索它,可以阅读这个链接:https://reactjs.org/docs/react-dom.html - Gorakh Nath
6个回答

119

根据 ReactDOMServer 文档(重点是我的):

如果你在一个已经拥有这个服务端渲染标记的节点上调用ReactDOM.hydrate()React将保留它并仅附加事件处理程序,从而使您拥有非常高效的首次加载体验。

粗体文本是主要区别。render 可能会更改您的节点,如果初始DOM和当前DOM之间存在差异。而hydrate只会附加事件处理程序。

根据Github问题引入hydrate作为单独API的说明

如果这是您的初始DOM:

<div id="container">
    <div class="spinner">Loading...</div>
</div>

然后调用:

ReactDOM.render(
   <div class="myapp">
      <span>App</span>
   </div>,
   document.getElementById('container')
)
打算进行仅限客户端渲染(而非水合作用)。然后你以此结束。
<div id="container">
   <div class="spinner">
       <span>App</span>
   </div>
</div>

因为我们没有修补这些属性。

只是提供信息,他们没有修补这些属性的原因是

... 这将在普通水化模式下导致速度变慢,并减缓首次呈现到非 SSR 树中。


19
我不明白为什么渲染后的 div 没有 class 名称为 myapp 的 div,以及为什么最终呈现的元素中有 spinner 类。 - pravin poudel
4
我认为这是因为在客户端渲染期间,它们没有修补属性。这就是为什么 class="spinner" 属性保留在 <div> 元素中的原因。 - Glenn Mohammad

66

关于使用hydrate的具体内容,我没有什么具体的补充,但是在尝试学习它时,我编写了一个小例子,所以这里是为那些发现它有帮助的人的工作。

目标

提供两个页面,一个使用ReactDOM.hydrate,另一个使用ReactDOM.render。它们将依赖一些用JSX编写的React组件,这些组件通过<script>标签加载,并由服务器进行人为延迟,以说明hydraterender之间的差异。

基本结构

  1. 一个包含HTML "框架"的文件
  2. 一个包含自定义的React组件的JSX文件
  3. 一个生成服务器所有页面的脚本文件
  4. 一个运行服务器的脚本文件

结果

生成页面并运行服务器后,我访问127.0.0.1,看到一个标题为hydrate的页面,一个按钮和两个链接。我可以点击按钮,但是没有任何反应。过了几秒钟,文档完成加载后,按钮开始计数我的点击次数。然后我点击"render"链接。现在,我看到的页面有一个标题为render和两个链接,但是没有按钮。几秒钟后,按钮出现并立即响应。

解释

在"hydrate"页面上,因为所有必要的HTML都随页面一起提供,所以所有标记都会立即呈现。按钮无响应,因为尚未连接任何回调函数。一旦components.js加载完成,window就会触发load事件,并使用hydrate连接回调。

在"render"页面上,按钮标记不随页面一起提供,而是仅由ReactDOM.render注入,因此它不会立即显示。请注意,脚本最终加载后页面的外观被突然更改。

源代码

这是我正在使用的自定义React组件。它将由node中的React静态渲染组件使用,也将从服务器动态加载以用于页面(这是在文件开头检查exportsReact对象的目的)。

// components.jsx

var exports = typeof(exports) == 'object' ? exports : {};
var React = typeof(React) == 'object' ? React : require('react');

function MyButton(props) {
  [click, setClick] = React.useState(0);
  function handleClick() { setClick(click + 1); }
  return (
    <button onClick={handleClick}>Clicked: {click}</button>
  );
}

exports.MyButton = MyButton;

这是用来生成服务器所有必需页面的脚本。首先,使用Babel将components.jsx转译为JavaScript,然后使用这些组件以及React和ReactDOMServer创建实际页面。这些页面由函数getPage创建,该函数从文件pageTemplate.js中导出,下面展示了该文件。

// genScript.js

let babel          = require('@babel/core');
let fs             = require('fs');
let ReactDOMServer = require('react-dom/server');
let React          = require('react');
let pageTemplate   = require('./pageTemplate.js');

script = babel.transformFileSync(
  'components.jsx', 
  {presets : [['@babel/react']]}
);

fs.writeFileSync('components.js',script.code);
let components = require('./components.js');

hydrateHTML = pageTemplate.getPage(
  'MyButton',
  ReactDOMServer.renderToString(React.createElement(components.MyButton)),
  'hydrate'
);

renderHTML = pageTemplate.getPage(
  'MyButton',
  '',
  'render'
);

fs.writeFileSync('hydrate.html',hydrateHTML);
fs.writeFileSync('render.html',renderHTML);
此文件仅导出先前提到的getPage函数。
// pageTemplate.js

exports.getPage = function(
  reactElementTag,
  reactElementString,
  reactDOMMethod
  ) {
  return `
  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="utf-8" />
      <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js" defer></script>
      <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" defer></script>
      <script src="./components.js" defer></script>
    </head>
    <body> 
      <h1>${ reactDOMMethod }</h1>
      <div id="react-root">${ reactElementString }</div> 
      <a href="hydrate.html">hydrate</a>
      <a href="render.html">render</a>
    </body>
    <script>
      window.addEventListener('load', (e) => {
        ReactDOM.${ reactDOMMethod }(
          React.createElement(${ reactElementTag }),
          document.getElementById('react-root')
        );
      });
    </script>
  </html>
  `;
}

最终,真正的服务器

// server.js

let http = require('http');
let fs   = require('fs');

let renderPage       = fs.readFileSync('render.html');
let hydratePage      = fs.readFileSync('hydrate.html');
let componentsSource = fs.readFileSync('components.js');

http.createServer((req, res) => {
  if (req.url == '/components.js') {
    // artificial delay
    setTimeout(() => {
    res.setHeader('Content-Type','text/javascript');
    res.end(componentsSource);
    }, 2000);
  } else if (req.url == '/render.html') {
    res.end(renderPage);
  } else {
    res.end(hydratePage);
  }
}).listen(80,'127.0.0.1');

9
哇,你基本回答了问题+解释了如何构建像Gatsby这样的最小静态网站生成器。太棒了,非常感谢! - Sergey Lukin

48

在 SSR(服务器端渲染)情况下,Hydrate 功能主要用于将 JS 添加至页面或应用 SSR 的节点。SSR 会提供骨架或 HTML 标记,这些标记从服务器发送,以便在页面首次加载时不会是空白的,并使搜索引擎机器人可以进行 SEO 索引(SSR 的一种用例)。因此,使用 Hydrate 可实现响应用户执行的事件。

Render 主要用于在客户端浏览器上渲染组件。如果您尝试使用 Render 替换 Hydrate,则会收到警告,说明 Render 已不建议在 SSR 情况下使用,这是因为相对于 Hydrate,它速度较慢而被删除了。


35

除了以上内容...

ReactDOM.hydrate()render() 相同,但它用于对由ReactDOMServer呈现的HTML内容进行水合作用(即附加事件侦听器)React将尝试将事件侦听器附加到现有标记

使用ReactDOM.render()来水合化服务器呈现的容器已经被弃用,因为速度较慢,并将在React 17中删除,所以请改用hydrate()


24
将已在服务器端渲染的 HTML 中重新添加功能的整个过程称为水合作用。
因此,将一次渲染的 HTML 再次重新渲染的过程称为水合作用。
因此,如果我们试图通过调用 ReactDOM.render() 来实现应用程序的水合作用,则应该使用调用 ReactDOM.hydrate()

7

render会清空指定元素(在大多数情况下命名为“root”)中的所有内容并重新构建它,而hydrate将保留已经存在于指定元素中的所有内容并从中构建,从而使初始页面加载更快。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接