38. React 18 有哪些重要更新?(自动批处理、并发特性、新的 Root API)

30 阅读5分钟

React 面试题详细答案 - 第 38 题

38. React 18 有哪些重要更新?(自动批处理、并发特性、新的 Root API)

React 18 概述

React 18 是 React 的一个重要版本,引入了许多新特性和改进,主要围绕并发渲染、自动批处理和新的 Root API 展开。

1. 自动批处理 (Automatic Batching)

什么是批处理?

批处理是指 React 将多个状态更新合并为一次重新渲染,以提高性能。

// React 17 及之前:只有 React 事件处理器中的更新会被批处理
function handleClick() {
  setCount((c) => c + 1)
  setFlag((f) => !f)
  // React 17:只会重新渲染一次
}

// React 17:Promise、setTimeout、原生事件处理器中的更新不会被批处理
setTimeout(() => {
  setCount((c) => c + 1)
  setFlag((f) => !f)
  // React 17:会重新渲染两次
}, 1000)

fetch('/api/data').then(() => {
  setCount((c) => c + 1)
  setFlag((f) => !f)
  // React 17:会重新渲染两次
})
React 18 的自动批处理
// React 18:所有更新都会被自动批处理
function handleClick() {
  setCount((c) => c + 1)
  setFlag((f) => !f)
  // React 18:只会重新渲染一次
}

setTimeout(() => {
  setCount((c) => c + 1)
  setFlag((f) => !f)
  // React 18:只会重新渲染一次
}, 1000)

fetch('/api/data').then(() => {
  setCount((c) => c + 1)
  setFlag((f) => !f)
  // React 18:只会重新渲染一次
})

// 如果需要立即更新,可以使用 flushSync
import { flushSync } from 'react-dom'

function handleClick() {
  flushSync(() => {
    setCount((c) => c + 1)
  })
  // 这里 count 已经更新
  flushSync(() => {
    setFlag((f) => !f)
  })
  // 这里 flag 已经更新
}

2. 并发特性 (Concurrent Features)

startTransition

startTransition 用于标记非紧急的更新,让 React 知道这些更新可以被中断。

import { startTransition, useState } from 'react'

function SearchResults() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, setIsPending] = useState(false)

  const handleSearch = (newQuery) => {
    setQuery(newQuery) // 紧急更新:立即更新输入框

    startTransition(() => {
      setIsPending(true)
      setResults([]) // 非紧急更新:清空结果

      // 模拟搜索
      searchAPI(newQuery).then((searchResults) => {
        setResults(searchResults)
        setIsPending(false)
      })
    })
  }

  return (
    <div>
      <input value={query} onChange={(e) => handleSearch(e.target.value)} />
      {isPending && <div>搜索中...</div>}
      <div>
        {results.map((result) => (
          <div key={result.id}>{result.title}</div>
        ))}
      </div>
    </div>
  )
}
useDeferredValue

useDeferredValue 用于延迟更新某个值,让其他更紧急的更新优先执行。

import { useDeferredValue, useState, useMemo } from 'react'

function ProductList({ products }) {
  const [filter, setFilter] = useState('')
  const deferredFilter = useDeferredValue(filter)

  const filteredProducts = useMemo(() => {
    return products.filter((product) =>
      product.name.toLowerCase().includes(deferredFilter.toLowerCase())
    )
  }, [products, deferredFilter])

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="搜索产品..."
      />
      <div>
        {filteredProducts.map((product) => (
          <div key={product.id}>{product.name}</div>
        ))}
      </div>
    </div>
  )
}
Suspense 改进

React 18 改进了 Suspense 的行为,支持更多的使用场景。

import { Suspense, lazy } from 'react'

// 懒加载组件
const LazyComponent = lazy(() => import('./LazyComponent'))

function App() {
  return (
    <div>
      <h1>我的应用</h1>
      <Suspense fallback={<div>加载中...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  )
}

// 数据获取的 Suspense
function UserProfile({ userId }) {
  const user = use(fetchUser(userId)) // 假设 use 是一个数据获取 Hook

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  )
}

function App() {
  return (
    <Suspense fallback={<div>加载用户信息...</div>}>
      <UserProfile userId="123" />
    </Suspense>
  )
}

3. 新的 Root API

createRoot

React 18 引入了新的 createRoot API 来替代 ReactDOM.render

// React 17 及之前
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(<App />, document.getElementById('root'))

// React 18
import { createRoot } from 'react-dom/client'
import App from './App'

const container = document.getElementById('root')
const root = createRoot(container)

root.render(<App />)

// 卸载应用
root.unmount()
hydrateRoot

用于服务端渲染的客户端水合。

// React 17
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.hydrate(<App />, document.getElementById('root'))

// React 18
import { hydrateRoot } from 'react-dom/client'
import App from './App'

const container = document.getElementById('root')
const root = hydrateRoot(container, <App />)

4. 新的 Hooks

useId

useId 用于生成唯一的 ID,特别适用于可访问性属性。

import { useId } from 'react'

function Form() {
  const nameId = useId()
  const emailId = useId()

  return (
    <form>
      <label htmlFor={nameId}>姓名</label>
      <input id={nameId} type="text" />

      <label htmlFor={emailId}>邮箱</label>
      <input id={emailId} type="email" />
    </form>
  )
}

// 在服务端和客户端生成相同的 ID
function Checkbox() {
  const id = useId()

  return (
    <>
      <input id={id} type="checkbox" />
      <label htmlFor={id}>同意条款</label>
    </>
  )
}
useSyncExternalStore

useSyncExternalStore 用于订阅外部数据源,确保在并发渲染时数据的一致性。

import { useSyncExternalStore } from 'react'

function useOnlineStatus() {
  return useSyncExternalStore(
    // 订阅函数
    (callback) => {
      window.addEventListener('online', callback)
      window.addEventListener('offline', callback)
      return () => {
        window.removeEventListener('online', callback)
        window.removeEventListener('offline', callback)
      }
    },
    // 获取快照函数
    () => navigator.onLine,
    // 服务端快照函数
    () => true
  )
}

function App() {
  const isOnline = useOnlineStatus()

  return (
    <div>
      <h1>{isOnline ? '在线' : '离线'}</h1>
    </div>
  )
}
useInsertionEffect

useInsertionEffect 用于在 DOM 更新之前插入样式,主要用于 CSS-in-JS 库。

import { useInsertionEffect } from 'react'

function useCSS(rule) {
  useInsertionEffect(() => {
    const style = document.createElement('style')
    style.textContent = rule
    document.head.appendChild(style)

    return () => {
      document.head.removeChild(style)
    }
  })
}

function MyComponent() {
  useCSS(`
    .my-component {
      color: red;
      font-size: 16px;
    }
  `)

  return <div className="my-component">Hello World</div>
}

5. 严格模式改进

React 18 的严格模式会故意双重调用组件、Effect 和状态更新函数,以帮助发现副作用。

import { StrictMode } from 'react'

function App() {
  return (
    <StrictMode>
      <MyComponent />
    </StrictMode>
  )
}

// 在严格模式下,以下函数会被调用两次
function MyComponent() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    console.log('Effect 运行') // 会被调用两次
  }, [])

  const handleClick = () => {
    setCount((c) => {
      console.log('状态更新') // 会被调用两次
      return c + 1
    })
  }

  return <button onClick={handleClick}>Count: {count}</button>
}

6. 实际应用示例

性能优化示例
import { startTransition, useDeferredValue, useState, useMemo } from 'react'

function SearchApp() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, setIsPending] = useState(false)

  const deferredQuery = useDeferredValue(query)

  const filteredResults = useMemo(() => {
    if (!deferredQuery) return []

    return results.filter((result) =>
      result.title.toLowerCase().includes(deferredQuery.toLowerCase())
    )
  }, [results, deferredQuery])

  const handleSearch = (newQuery) => {
    setQuery(newQuery) // 立即更新输入框

    startTransition(() => {
      setIsPending(true)

      // 模拟 API 调用
      fetch(`/api/search?q=${newQuery}`)
        .then((res) => res.json())
        .then((data) => {
          setResults(data)
          setIsPending(false)
        })
    })
  }

  return (
    <div>
      <input
        value={query}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="搜索..."
      />

      {isPending && <div>搜索中...</div>}

      <div>
        {filteredResults.map((result) => (
          <div key={result.id}>{result.title}</div>
        ))}
      </div>
    </div>
  )
}
并发渲染示例
import { startTransition, useState, useTransition } from 'react'

function TabContainer() {
  const [tab, setTab] = useState('home')
  const [isPending, startTransition] = useTransition()

  const handleTabChange = (newTab) => {
    startTransition(() => {
      setTab(newTab)
    })
  }

  return (
    <div>
      <div>
        <button
          onClick={() => handleTabChange('home')}
          style={{ opacity: isPending ? 0.5 : 1 }}
        >
          首页
        </button>
        <button
          onClick={() => handleTabChange('about')}
          style={{ opacity: isPending ? 0.5 : 1 }}
        >
          关于
        </button>
        <button
          onClick={() => handleTabChange('contact')}
          style={{ opacity: isPending ? 0.5 : 1 }}
        >
          联系
        </button>
      </div>

      <div>
        {tab === 'home' && <HomeTab />}
        {tab === 'about' && <AboutTab />}
        {tab === 'contact' && <ContactTab />}
      </div>
    </div>
  )
}

7. 迁移指南

从 React 17 迁移到 React 18
// 1. 更新 Root API
// 旧代码
import ReactDOM from 'react-dom'
ReactDOM.render(<App />, document.getElementById('root'))

// 新代码
import { createRoot } from 'react-dom/client'
const root = createRoot(document.getElementById('root'))
root.render(<App />)

// 2. 处理自动批处理
// 如果某些更新需要立即执行,使用 flushSync
import { flushSync } from 'react-dom'

function handleClick() {
  flushSync(() => {
    setCount((c) => c + 1)
  })
  // count 已经更新
}

// 3. 使用新的 Hooks
// 替换自定义 ID 生成逻辑
const id = useId() // 替代 Math.random() 或自定义 ID 生成

// 4. 更新第三方库
// 确保使用的第三方库支持 React 18

总结

React 18 的主要更新包括:

  1. 自动批处理:所有更新都会被自动批处理,提高性能
  2. 并发特性:startTransition、useDeferredValue 等新 API 支持并发渲染
  3. 新的 Root API:createRoot 和 hydrateRoot 替代旧的 API
  4. 新的 Hooks:useId、useSyncExternalStore、useInsertionEffect
  5. 严格模式改进:帮助发现副作用问题
  6. Suspense 改进:支持更多使用场景

这些更新让 React 应用具有更好的性能、用户体验和开发体验,特别是在处理大量数据和复杂交互时。