说说 React render 方法的原理?在什么时候会被触发?

一、是什么

在 React 中,render 是将组件转化为虚拟 DOM(React Element)的核心过程。它描述了"当前状态下 UI 应该长什么样",而非直接操作真实 DOM。React 通过比较前后两次 render 的结果(Reconciliation),计算出最小 DOM 变更并应用到页面上。

对于类组件,render 是 render() 方法;对于函数组件,整个函数体本身就是 render 过程。

// 类组件:render() 方法返回 React Element
class Greeting extends React.Component<{ name: string }> {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

// 函数组件:函数返回值即 render 结果
function Greeting({ name }: { name: string }) {
  return <h1>Hello, {name}</h1>;
}

两者都返回 React Element,它是一个描述 UI 结构的普通 JavaScript 对象:

// <h1>Hello, React</h1> 在运行时是这样的对象
{
  type: 'h1',
  props: { children: 'Hello, React' },
  key: null,
  ref: null,
}

二、render 的触发时机

2.1 初次渲染

应用首次挂载时,React 18 使用 createRoot API:

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

const root = createRoot(document.getElementById('root')!);
root.render(<App />);

此时 React 会从根组件开始,递归调用所有子组件的 render,构建完整的虚拟 DOM 树,然后一次性创建真实 DOM 并挂载到页面。

2.2 setState / useState 更新

当组件调用 setState(类组件)或 useState 的 setter(函数组件)时,会标记该组件需要重新渲染:

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

  const handleClick = () => {
    setCount(prev => prev + 1); // 触发重新渲染
  };

  console.log('Counter render'); // 每次状态变化都会执行

  return <button onClick={handleClick}>{count}</button>;
}

React 18 中所有的状态更新都会被自动批处理(Automatic Batching),即使在 setTimeout、Promise 或原生事件中:

function BatchExample() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  const handleClick = () => {
    // React 18:这两次更新被合并为一次 render
    setTimeout(() => {
      setCount(c => c + 1);
      setFlag(f => !f);
      // 只触发一次重新渲染
    }, 0);
  };

  return <div>{count} - {String(flag)}</div>;
}

2.3 Props 变化

当父组件重新渲染时,会重新创建子组件的 props 对象。即使 props 值没变,子组件默认也会重新渲染:

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

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      {/* Parent 每次 render,Child 也会 render,即使 name 没变 */}
      <Child name="React" />
    </div>
  );
}

function Child({ name }: { name: string }) {
  console.log('Child render'); // 每次父组件更新都会打印
  return <span>{name}</span>;
}

2.4 forceUpdate(类组件)

类组件可以通过 this.forceUpdate() 强制跳过 shouldComponentUpdate 检查,直接触发重新渲染:

class DataDisplay extends React.Component {
  externalData = externalStore.getData();

  refresh = () => {
    this.externalData = externalStore.getData();
    this.forceUpdate(); // 跳过 shouldComponentUpdate
  };

  render() {
    return <div>{this.externalData.value}</div>;
  }
}

函数组件中可以用 useReducer 模拟类似效果:

function useForceUpdate() {
  const [, forceUpdate] = useReducer(x => x + 1, 0);
  return forceUpdate;
}

2.5 Context 变化

Context.Providervalue 发生变化时,所有消费该 Context 的组件都会重新渲染,无视 React.memo 等优化措施:

const ThemeContext = createContext<'light' | 'dark'>('light');

function App() {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  return (
    <ThemeContext.Provider value={theme}>
      <Header />
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        切换主题
      </button>
    </ThemeContext.Provider>
  );
}

// 即使被 React.memo 包裹,context 变化时仍会重新渲染
const ThemedButton = React.memo(function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>按钮</button>;
});

2.6 父组件重新渲染

React 的默认行为是:父组件重新渲染时,所有子组件都会重新渲染,无论子组件的 props 是否变化。这是许多性能问题的根源,也是 React.memouseMemouseCallback 存在的原因。

三、React 18 createRoot 与 Legacy render

React 18 引入了新的根节点 API,以支持并发特性:

// React 18 新 API — 启用并发特性
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root')!);
root.render(<App />);

// Legacy API — 不支持并发特性
import { render } from 'react-dom';
render(<App />, document.getElementById('root'));

两者的关键区别:

特性createRootLegacy render
自动批处理所有场景都启用仅在 React 事件中
并发特性支持 useTransition 等不支持
Suspense 行为完整支持部分支持
卸载方式root.unmount()unmountComponentAtNode()

四、并发渲染的影响

React 18 引入的并发渲染(Concurrent Rendering)改变了 render 的执行方式。在传统的同步模式下,一旦 render 开始就必须完成整棵树才能交出控制权。并发模式下 render 可以被中断和恢复:

import { useTransition, useState } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<string[]>([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setQuery(value); // 高优先级:立即更新输入框

    startTransition(() => {
      setResults(filterLargeList(value)); // 低优先级:可以被中断
    });
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <span>搜索中...</span>}
      <ul>
        {results.map(item => <li key={item}>{item}</li>)}
      </ul>
    </div>
  );
}

并发渲染让 React 可以同时准备多个版本的 UI,优先响应用户交互,将低优先级的渲染工作延后处理。

五、render 阶段 vs commit 阶段

React 的更新过程分为两个阶段:

render 阶段(可中断)
├── 调用组件函数 / render 方法
├── 生成新的虚拟 DOM
├── Diff 比较(Reconciliation)
└── 计算需要的 DOM 变更(Effect List)

commit 阶段(不可中断)
├── 执行 DOM 操作(增、删、改)
├── 调用 useLayoutEffect / componentDidMount / componentDidUpdate
└── 异步调度 useEffect

render 阶段是纯计算过程,不产生副作用,因此在并发模式下可以被中断和重新开始。commit 阶段涉及真实 DOM 操作,必须同步完成以保证 UI 一致性。

六、总结

render 方法的核心职责是根据当前 state 和 props 返回 UI 描述(React Element)。它会在以下时机被触发:

  1. 初次挂载createRoot().render() 调用
  2. 状态更新setStateuseState setter
  3. Props 变化 — 父组件传递新的 props
  4. forceUpdate — 类组件强制更新
  5. Context 变化 — Provider value 更新
  6. 父组件渲染 — 默认级联触发子组件渲染

React 18 的并发渲染让 render 过程变得可中断,配合自动批处理大幅减少了不必要的渲染次数。理解 render 的触发机制是进行性能优化的基础,只有知道为什么组件会重新渲染,才能有针对性地阻止不必要的渲染。