React中的渲染机制--从组件树到DOM元素

214 阅读6分钟

前言

最近我在阅读React的官方文档时,对React的渲染和提交机制有了更深入的理解。作为前端开发者,我们每天都在写组件,但很少思考这些组件是如何最终变成屏幕上可见的UI的。本文将分享我对React渲染机制的理解,希望能帮助大家更好地掌握React的核心工作原理。

什么是渲染

渲染就是将我们编写的React组件转换为最终显示在屏幕上的内容。我们平时开发时,写完代码启动项目往往就能看到效果,往往忽略了React在背后完成的复杂工作。

举个例子,我们编写一个简单的登录表单组件:

function LoginForm() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  return (
    <form>
      <input 
        type="text" 
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">登录</button>
    </form>
  );
}

这个组件需要经过React的渲染流程,才能最终在页面上显示为可交互的表单。整个过程对开发者是透明的,但理解其中的机制对性能优化和问题排查至关重要。


渲染的过程

React的渲染过程可以分为以下几个关键步骤:

一、触发渲染

有两种主要方式会触发渲染:

1. 初始化触发渲染(初次渲染)

当应用启动时,会触发初次渲染。框架和沙箱有时会隐藏这部分代码,但它的底层是通过调用 createRoot 方法并传入目标 DOM 节点,然后用你的组件调用 render 函数完成的:

import Image from './Image.js';
import { createRoot } from 'react-dom/client';

// 初次渲染的过程
const root = createRoot(document.getElementById('root'))
root.render(<Image />); 

export default function Image() {
  return (
    <img
      src="https://i.imgur.com/ZF6s192.jpg"
      alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
    />
  );
}

2. 状态变化触发渲染(重渲染)

自身状态变化:当组件内部的状态(state)更新时

const [count, setCount] = useState(0);
// 调用setCount会触发重新渲染
setCount(count + 1);

父组件状态变化:父组件重新渲染会导致所有子组件重新渲染(即使子组件的props没有变化)

二、虚拟DOM生成

React会执行组件函数,生成虚拟DOM树。虚拟DOM是轻量级的JavaScript对象,描述UI应该是什么样子:

// JSX会被编译为React.createElement调用
<div className="title">Hello</div>

// 转换为虚拟DOM对象
{
  type: 'div',
  props: { className: 'title', children: 'Hello' }
}

三、协调

React 将新生成的虚拟 DOM 与上一次渲染的虚拟 DOM 进行对比(Diffing 算法),找出需要更新的部分:

  • 差异计算:识别哪些节点需要添加、修改或删除。
  • 优化策略:React 使用高效的算法(如基于 key 的列表对比)最小化操作。

四、提交到真实DOM

在渲染(调用)你的组件之后,React 将会修改 DOM。

对于初次渲染,React 会使用 appendChild() DOM API 将其创建的所有 DOM 节点放在屏幕上。

对于重渲染,React 将应用最少的必要操作(在渲染时计算!),以使得 DOM 与最新的渲染输出相互匹配。

五、浏览器渲染(用户看到页面)

浏览器处理DOM更新,计算样式(CSSOM)、布局(Layout)、绘制(Paint),最终显示在屏幕上。

需要注意

上面过程中需要注意几点

渲染 ≠ 直接更新真实 DOM

React的渲染过程实际上分为"渲染阶段"(生成虚拟DOM)和"提交阶段"(更新真实DOM)。组件可能在渲染阶段被执行多次,但只有最终结果会被提交到DOM,整个过程是按步骤的,而不是直接更新正式DOM

什么是虚拟DOM

虚拟DOM(Virtual DOM)是React的核心概念之一,它是一个轻量级的JavaScript对象,用于描述真实DOM(文档对象模型)的结构和状态。虚拟DOM并不是真实的浏览器DOM元素,而是React在内存中构建的DOM的抽象表示。

什么是真实DOM

真实DOM 元素(Document Object Model Element)  是浏览器对 HTML 文档中所有标签(如 <div><p><button> 等)的抽象表示。他既包含了HTML标签结构,也包含随之关联的CSS样式和JavaScript行为。

React 通过 虚拟 DOM(Virtual DOM)  间接操作真实 DOM,优化性能:

  1. 虚拟 DOM:React 组件返回的 JSX 会转换为轻量级的 JavaScript 对象(虚拟 DOM)。
  2. 对比更新:当状态变化时,React 生成新的虚拟 DOM,与旧的对比(Diffing),找出最小变化。
  3. 更新真实 DOM:仅将变化的部分应用到真实 DOM 上。

为什么需要这样子做?

在传统的前端开发中,直接操作真实DOM存在几个关键问题:

  1. DOM操作成本高昂:每次DOM更新都会触发浏览器的重排(Reflow)和重绘(Repaint),频繁操作会导致性能下降
  2. 手动优化困难:开发者需要自己跟踪哪些DOM需要更新,容易出错
  3. 跨平台兼容性问题:不同浏览器对DOM的实现有差异

Diffing算法是如何高效更新的?

React的Diffing算法遵循以下原则:

  1. 同层比较:只比较同一层级的节点
  2. 类型不同则重建:如果组件类型改变,直接重建整个子树
  3. key的重要性:列表项使用稳定的key可以提高比较效率
// 不好的做法:使用index作为key
{todos.map((todo, index) => (
  <Todo key={index} {...todo} />
))}

// 好的做法:使用唯一id作为key
{todos.map((todo) => (
  <Todo key={todo.id} {...todo} />
)}


如何优化React渲染性能

  1. 避免不必要的渲染

    • 使用React.memo缓存函数组件
    • 类组件继承PureComponent
    • 合理使用shouldComponentUpdate
  2. 优化状态更新

    • 合并多个setState调用
    • 使用useMemo缓存计算结果
    • 使用useCallback缓存函数引用
  3. 代码分割

    • 使用React.lazy动态加载组件
    • 配合Suspense提供加载状态
  4. 列表优化

    • 为列表项设置唯一的key
    • 考虑使用虚拟滚动技术处理长列表
  5. 合理使用Context

    • 将Context的值拆分为多个Context
    • 避免在Context中放置频繁变化的值

结语

理解React的渲染机制是成为高级React开发者的关键一步。通过掌握虚拟DOM、协调过程和Diffing算法的工作原理,我们可以编写出性能更好的React应用。希望本文能帮助你更深入地理解React的渲染机制,在实际开发中做出更明智的架构和优化决策。

如果你对React渲染机制有任何问题或见解,欢迎在评论区留言讨论!