React 16-19 新特性

React 16 是一个分水岭,从这个版本开始 React 进行了底层架构重写,并在后续版本中持续引入了大量新特性。从 React 16 到 React 19 按版本线做一个系统的讲解。


React 16:架构革新

Fiber 架构

React 16 最核心的变化是将底层的渲染引擎从 Stack Reconciler 重写为 Fiber Reconciler。

之前的 Stack Reconciler 是同步递归的------一旦开始 reconciliation(比较新旧虚拟 DOM 树),就必须一口气递归完整棵树才能停下来。如果组件树很大(比如渲染一个几千行的数据表格),这个过程可能占据主线程几十甚至上百毫秒,期间浏览器无法响应用户输入、无法做动画渲染,页面就会"卡死"。

Fiber 架构把渲染工作拆分成一个个小的工作单元(Fiber Node),每个 Fiber 对应一个组件。每完成一个工作单元后,React 会检查是否还有剩余时间(通过类似 requestIdleCallback 的调度机制),如果时间不够就暂停工作、把控制权交还给浏览器做渲染和事件处理,等下一帧再继续。这就实现了"可中断的异步渲染"。

Fiber 架构将渲染过程分为两个阶段。Render Phase(可中断)负责构建新的 Fiber 树、做 diff 对比、标记需要变更的节点,这个阶段不会产生任何可见的 DOM 操作。Commit Phase(不可中断)负责将 Render Phase 收集的变更一次性应用到真实 DOM 上,必须同步完成以保证 UI 的一致性。

Error Boundaries(错误边界)

React 16 之前,组件渲染过程中的 JS 错误会导致整个组件树崩溃,页面白屏且无法恢复。Error Boundary 是一种特殊的类组件,通过 componentDidCatch 和 static getDerivedStateFromError 捕获子组件树中渲染、生命周期方法和构造函数中的错误,展示降级 UI 而不是白屏。

javascript 复制代码
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    // 更新 state,下次渲染时展示降级 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 上报错误到监控系统
    logErrorToService(error, errorInfo.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return <h1>页面出了点问题,请刷新重试</h1>;
    }
    return this.props.children;
  }
}

// 使用
<ErrorBoundary>
  <RiskyComponent />
</ErrorBoundary>

需要注意 Error Boundary 无法捕获以下场景的错误:事件处理函数中的错误(用普通 try-catch)、异步代码(setTimeout、Promise)、服务端渲染、Error Boundary 自身的错误。

Portals

ReactDOM.createPortal(child, container) 允许将子组件渲染到 DOM 树中任意位置的节点上,而不仅仅是父组件的 DOM 子树中。这对于 Modal、Tooltip、Popover 等需要"突破"父组件 overflow: hidden 或 z-index 限制的场景非常有用。

虽然 Portal 在 DOM 结构上渲染到了外部节点,但在 React 的组件树中它仍然是父组件的子组件------事件冒泡、Context 传递等行为和普通子组件完全一致。

新的生命周期

React 16.3 废弃了三个"不安全"的生命周期方法,并引入了两个新的。

被废弃的:componentWillMount、componentWillReceiveProps、componentWillUpdate。这三个方法之所以被废弃,是因为 Fiber 架构的 Render Phase 可以被中断和重试,这意味着这些方法可能在一次更新中被调用多次。如果开发者在这些方法中做了副作用操作(发请求、订阅事件、修改 DOM),就会产生重复执行的 bug。

新增的 static getDerivedStateFromError(nextProps) 是一个静态方法,返回一个对象来更新 state,用于在 props 变化时派生 state。它是纯函数,没有副作用风险。getSnapshotBeforeUpdate(prevProps, prevState) 在 DOM 更新之前调用,返回值会作为 componentDidUpdate 的第三个参数。典型用途是在 DOM 变化前保存滚动位置等信息。

Fragment

允许组件返回多个元素而不需要额外的 DOM 包裹节点:

jsx 复制代码
// 短语法
return (
  <>
    <Header />
    <Main />
    <Footer />
  </>
);

// 需要传 key 时用完整语法
return (
  <React.Fragment key={item.id}>
    <dt>{item.term}</dt>
    <dd>{item.description}</dd>
  </React.Fragment>
);

Context API 重新设计

React 16.3 引入了全新的 Context API,取代了之前实验性的旧版 Context。新 API 通过 React.createContext 创建上下文,Provider 提供值,Consumer(或后来的 useContext)消费值。当 Provider 的 value 变化时,所有消费该 Context 的后代组件都会重新渲染,不受 shouldComponentUpdate 的影响------这保证了 Context 值的变化不会被中间组件的优化"拦截"。


React 16.8:Hooks 革命

Hooks 是 React 最重要的特性变更之一,它从根本上改变了 React 的编程范式------从"类组件 + 生命周期"转向"函数组件 + Hooks"。

设计动机

类组件存在几个根本性的问题。逻辑复用困难------要在组件间共享状态逻辑,只能用 HOC(高阶组件)或 render props,导致"嵌套地狱"和组件层级膨胀。逻辑分散------同一个关注点的代码被拆到不同的生命周期方法中(componentDidMount 订阅、componentDidUpdate 更新、componentWillUnmount 清理),而不相关的逻辑又被塞在同一个生命周期方法中。class 本身的问题------this 绑定令人困惑,class 不能很好地被压缩,对未来的编译优化不友好。

useState

最基础的 Hook,为函数组件添加状态:

jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

useState 返回当前状态值和一个更新函数。更新函数可以接收新值,也可以接收一个函数(函数式更新),后者在新状态依赖旧状态时更安全:

jsx 复制代码
// 基于旧状态更新,避免闭包陷阱
setCount(prev => prev + 1);

useEffect

将副作用(数据获取、订阅、DOM 操作等)从生命周期方法中解耦出来:

jsx 复制代码
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 副作用:获取用户数据
    let cancelled = false;
    
    fetchUser(userId).then(data => {
      if (!cancelled) setUser(data);
    });

    // 清理函数:组件卸载或 userId 变化时执行
    return () => { cancelled = true; };
  }, [userId]); // 依赖数组:只在 userId 变化时重新执行

  return user ? <div>{user.name}</div> : <div>Loading...</div>;
}

依赖数组的行为:不传依赖数组------每次渲染后都执行;空数组 []------只在挂载时执行一次,相当于 componentDidMount;有依赖项------只在依赖项变化时执行。

useEffect 的执行时机是在 DOM 更新之后异步执行(不阻塞浏览器绘制)。如果需要在 DOM 更新后同步执行(比如需要读取 DOM 布局信息然后同步修改 DOM 避免闪烁),用 useLayoutEffect。

useContext

简化 Context 的消费,替代 Consumer 组件:

jsx 复制代码
const ThemeContext = React.createContext('light');

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>Click me</button>;
}

useRef

创建一个在整个组件生命周期内保持不变的可变引用对象。两个主要用途:访问 DOM 节点(ref.current 指向 DOM 元素),以及存储不触发重渲染的可变值(比如定时器 ID、上一次的 props 值等):

jsx 复制代码
function TextInput() {
  const inputRef = useRef(null);
  
  const focusInput = () => {
    inputRef.current.focus();
  };

  return <input ref={inputRef} />;
}

useReducer

适合管理复杂状态逻辑的 Hook,类似 Redux 的 reducer 模式:

jsx 复制代码
function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    default: throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  );
}

useMemo 和 useCallback

性能优化 Hook。useMemo 缓存计算结果,只在依赖变化时重新计算。useCallback 缓存函数引用,避免子组件因为父组件重渲染时获得新的函数引用而不必要地重渲染。

jsx 复制代码
function ExpensiveList({ items, filter }) {
  // 只在 items 或 filter 变化时重新过滤和排序
  const processedItems = useMemo(() => {
    return items.filter(filter).sort(compareFn);
  }, [items, filter]);

  // 只在没有依赖变化时保持同一个函数引用
  const handleClick = useCallback((id) => {
    console.log('clicked', id);
  }, []);

  return processedItems.map(item => (
    <ListItem key={item.id} item={item} onClick={handleClick} />
  ));
}

自定义 Hook

Hooks 解决逻辑复用问题的核心方式。自定义 Hook 就是一个以 use 开头的函数,内部可以调用其他 Hook:

jsx 复制代码
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

// 任何组件都能复用这个逻辑
function MyComponent() {
  const { width, height } = useWindowSize();
  return <div>{width} x {height}</div>;
}

React 17:无新特性的过渡版本

React 17 没有面向开发者的新特性,但做了两个重要的底层改变。

事件委托从 document 改为根节点。 React 17 之前,所有事件都委托到 document 上。React 17 改为委托到 ReactDOM.render 挂载的根 DOM 节点。这个改变的目的是支持"渐进式升级"------同一个页面中可以运行多个不同版本的 React(比如旧系统用 React 16,新模块用 React 18),它们的事件系统不会互相干扰。

移除事件池(Event Pooling)。 React 16 中合成事件对象会被回收复用(事件回调执行完后事件对象的属性被清空),如果要在异步代码中访问事件属性需要调用 e.persist()。React 17 移除了这个机制,事件对象不再被池化,可以自由地在异步代码中使用。


React 18:并发渲染

React 18 是继 React 16 之后的又一次重大架构升级,核心是将 Fiber 架构的"可中断渲染"能力真正暴露给开发者。

Concurrent Rendering(并发渲染)

Fiber 架构在 React 16 就引入了,但在 React 16-17 中并没有真正启用并发能力------所有更新仍然是同步的。React 18 通过新的 createRoot API 开启并发模式:

jsx 复制代码
// React 17
ReactDOM.render(<App />, document.getElementById('root'));

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

并发渲染的核心能力是:React 可以同时准备多个版本的 UI,高优先级的更新(用户输入)可以中断低优先级的更新(数据加载后的大列表渲染),让页面在任何情况下都保持响应。

useTransition

将某些更新标记为"低优先级的过渡更新",允许被高优先级更新(用户输入)中断:

jsx 复制代码
function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    // 输入框更新是高优先级的,立即响应
    setQuery(e.target.value);
    
    // 搜索结果更新是低优先级的,可以被中断
    startTransition(() => {
      setResults(performSearch(e.target.value));
    });
  };

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending ? <Spinner /> : <ResultList results={results} />}
    </>
  );
}

用户快速连续输入时,每次输入都会中断上一次的搜索结果渲染(因为它在 transition 中),但输入框本身始终流畅响应。这在 React 18 之前需要手动做 debounce 来实现类似效果。

useDeferredValue

将某个值标记为"可延迟"的,React 会在空闲时才用新值重新渲染,繁忙时继续使用旧值:

jsx 复制代码
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery; // 当前显示的是旧值

  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      <Results query={deferredQuery} />
    </div>
  );
}

useDeferredValue 和 useTransition 的区别在于:useTransition 包裹的是"触发更新的动作"(你控制的是 setState 的优先级),useDeferredValue 包裹的是"消费更新的值"(你控制不了 setState 的调用,但可以延迟使用它的结果)。

Automatic Batching(自动批处理)

React 18 之前,只有 React 事件处理函数中的多次 setState 会被批处理合并为一次重渲染。在 setTimeout、Promise、原生事件回调中的 setState 每次都会触发独立的重渲染。React 18 的自动批处理将这个能力扩展到了所有场景:

jsx 复制代码
// React 18 之前:setTimeout 中的两次 setState 触发两次重渲染
setTimeout(() => {
  setCount(c => c + 1); // 触发一次渲染
  setFlag(f => !f);     // 再触发一次渲染
}, 1000);

// React 18:自动批处理,只触发一次重渲染
setTimeout(() => {
  setCount(c => c + 1); // 不立即渲染
  setFlag(f => !f);     // 不立即渲染
  // 统一渲染一次
}, 1000);

如果需要强制同步更新(极少数场景),可以用 flushSync。

Suspense 增强与流式 SSR

Suspense 在 React 16.6 就引入了但只支持 React.lazy 的代码分割场景。React 18 大幅增强了 Suspense 的能力。

在服务端渲染(SSR)场景下,传统 SSR 是"全有或全无"的------服务端必须等所有数据获取完成、整个页面渲染完毕后才能发送 HTML。React 18 的流式 SSR(Streaming SSR)配合 Suspense,可以先发送已经准备好的部分 HTML,数据还没准备好的部分用 Suspense 的 fallback 占位,等数据就绪后再"流式地"发送这部分 HTML 并通过 JS 注入到页面中。用户能更快地看到页面内容并开始交互。

jsx 复制代码
<Suspense fallback={<Spinner />}>
  <SlowDataComponent /> {/* 数据获取慢的组件 */}
</Suspense>

useId

在 SSR 场景下生成稳定的唯一 ID,保证服务端和客户端渲染的 ID 一致(避免 hydration mismatch):

jsx 复制代码
function FormField({ label }) {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </>
  );
}

useSyncExternalStore

为第三方状态管理库(Redux、Zustand 等)提供的 Hook,保证在并发渲染下从外部 store 读取数据时的一致性(避免"撕裂"问题------同一次渲染中不同组件读到了 store 的不同版本的值):

jsx 复制代码
const state = useSyncExternalStore(
  store.subscribe,      // 订阅函数
  store.getSnapshot,    // 获取当前值
  store.getServerSnapshot // SSR 时获取值
);

useInsertionEffect

专为 CSS-in-JS 库设计的 Hook,执行时机在 DOM 变更之后、useLayoutEffect 之前。CSS-in-JS 库需要在这个时机向 DOM 中注入 <style> 标签,保证 useLayoutEffect 中读取布局信息时样式已经生效。普通开发者基本不会直接使用。


React 19:最新特性

React Compiler

React 19 最具颠覆性的特性是 React Compiler(原名 React Forget)。它是一个编译时优化工具,能够自动为你的组件添加 memoization------本质上就是自动帮你加 useMemo、useCallback 和 React.memo。

在 React 19 之前,开发者需要手动判断哪里需要优化并添加 useMemo/useCallback,这既增加了心智负担,又容易用错(依赖数组写漏/写多)。React Compiler 通过在编译时分析组件的数据流,自动识别哪些值在 props/state 不变时可以复用,并注入缓存逻辑。开发者不再需要写任何手动优化代码。

Actions 和 useActionState

React 19 引入了 Actions 的概念,简化了表单提交和异步操作的处理。useActionState 将异步操作的状态管理(pending、error、result)封装为 Hook:

jsx 复制代码
function ChangeName() {
  const [state, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const name = formData.get('name');
      const error = await updateName(name);
      if (error) return { error };
      return { success: true };
    },
    { error: null }
  );

  return (
    <form action={submitAction}>
      <input name="name" />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Saving...' : 'Save'}
      </button>
      {state.error && <p>{state.error}</p>}
    </form>
  );
}

useOptimistic

支持乐观更新------在异步操作完成之前先展示预期的结果,如果操作失败再回滚:

jsx 复制代码
function MessageList({ messages, sendMessage }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (currentMessages, newMessage) => [
      ...currentMessages,
      { text: newMessage, sending: true }
    ]
  );

  async function handleSend(formData) {
    const text = formData.get('message');
    addOptimisticMessage(text); // 立即展示(乐观更新)
    await sendMessage(text);    // 实际发送
  }

  return (
    <>
      {optimisticMessages.map(msg => (
        <div style={{ opacity: msg.sending ? 0.5 : 1 }}>{msg.text}</div>
      ))}
      <form action={handleSend}>
        <input name="message" />
      </form>
    </>
  );
}

use

一个新的 API,可以在组件渲染过程中读取 Promise 或 Context 的值。和其他 Hook 不同,use 可以在条件语句中调用:

jsx 复制代码
function UserProfile({ userPromise }) {
  const user = use(userPromise); // 等同于 Suspense 的数据获取
  return <div>{user.name}</div>;
}

// 条件读取 Context
function Theme({ isLoggedIn }) {
  if (isLoggedIn) {
    const theme = use(ThemeContext);
    return <div className={theme} />;
  }
  return <div>Please log in</div>;
}

ref 作为 prop

React 19 之前,如果想把 ref 传给函数组件,必须用 forwardRef 包裹。React 19 允许函数组件直接接收 ref 作为普通 prop:

jsx 复制代码
// React 19 之前
const Input = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

// React 19
function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

forwardRef 在未来版本中将被废弃。

改进的 Context

React 19 中可以直接用 <Context> 作为 Provider,不再需要 <Context.Provider>:

jsx 复制代码
const ThemeContext = createContext('light');

// React 19 之前
<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>

// React 19
<ThemeContext value="dark">
  <App />
</ThemeContext>

文档元数据支持

React 19 允许在组件中直接渲染 <title>、<meta>、<link> 标签,React 会自动将它们提升到 <head> 中:

jsx 复制代码
function BlogPost({ post }) {
  return (
    <>
      <title>{post.title}</title>
      <meta name="description" content={post.summary} />
      <article>{post.content}</article>
    </>
  );
}

不再需要 react-helmet 等第三方库来管理文档元数据。

样式表优先级控制

React 19 支持声明式的样式表加载和优先级控制:

jsx 复制代码
function Component() {
  return (
    <>
      <link rel="stylesheet" href="base.css" precedence="default" />
      <link rel="stylesheet" href="theme.css" precedence="high" />
      <div>content</div>
    </>
  );
}

React 会保证样式表在组件内容显示之前加载完毕(配合 Suspense),并按照 precedence 属性控制样式表的插入顺序。


版本特性一览表

版本 核心特性
16.0 Fiber 架构、Error Boundaries、Portals、Fragment
16.3 新 Context API、新生命周期、createRef、forwardRef
16.6 React.lazy、Suspense(代码分割)、React.memo
16.8 Hooks(useState、useEffect、useContext、useRef、useReducer、useMemo、useCallback、自定义 Hook)
17 事件委托改到根节点、移除事件池、渐进式升级支持
18 并发渲染(createRoot)、useTransition、useDeferredValue、自动批处理、Suspense 增强、流式 SSR、useId、useSyncExternalStore
19 React Compiler、Actions、useActionState、useOptimistic、use API、ref 作为 prop、文档元数据支持、改进的 Context

以上从 React 16 到 19 按版本线完整梳理了所有重要特性。面试中最高频的考点集中在 Fiber 架构原理、Hooks 的设计动机和实现约束、并发渲染(useTransition/useDeferredValue)的使用场景、以及 React Compiler 对性能优化范式的影响。如果需要针对某个特性继续深挖,随时告诉我。

相关推荐
liuchangng5 小时前
React 19 + Vite 6 实战:从教材 PDF 到 866 词间隔重复单词 App
前端·react.js·pdf·vite·开源项目
鸿蒙开发18 小时前
Three.js 首帧卡顿排查:compileAsync 之后,为什么还会停一下?
react.js·three.js
小李科技1 天前
【性能优化】事件委托:目标定位与 React/Vue 实战
前端·vue.js·react.js·面试·性能优化·前端框架
csj501 天前
前端基础之《React(16)—Modal组件封装》
前端·react.js
ZoeLandia3 天前
React11:数据请求与 TanStack Query
前端·react.js·typescript·react
瓦瓦gogo3 天前
实习中接触前端开发后,我开始了解 React
前端·react.js·前端框架
ZoeLandia4 天前
React08:自定义 Hook
开发语言·前端·react.js·typescript
空中湖6 天前
用 React 写数据大屏:为什么我不建议用图表库
前端·react.js·前端框架
来一首_We_go7 天前
React Flow Pro 高级交互实现
前端·react.js
weedsfly7 天前
React 不可变数据:为什么页面“明明数据变了却不更新
前端·javascript·react.js