React 18中的新内容(超详细介绍)

549 阅读11分钟

一些令人兴奋的新的改进已经随着React 18的推出而推出。当React 18在一年前宣布时,该团队承诺采取渐进式的采用策略。现在,一年后,这正是他们所做的,你可以将你的应用程序升级到最新的版本。

React 18带来了一些突破性的变化,这取决于你如何使用它。但总的来说,它也带来了开箱即用的性能改进,包括默认的批处理,这消除了在应用程序或库代码中手动批处理更新的需要。

对于一些人来说,这是对他们的音乐,其他人可能需要更多的说服力。因此,让我们深入了解Facebook团队带给我们的一些最重要的新变化。

React 18中的突破性变化

一个重要的版本如果没有突破性的变化会是什么?嗯,这个版本的React有点不同,你马上就会明白为什么。你可以做的改变之一是将render 改为createRoot ,像这样:

// Before
import { render } from "react-dom"

const container = document.getElementById("app")
render(<App tab="home" />, container)

// After
import { createRoot } from "react-dom/client"

const container = document.getElementById("app")
const root = createRoot(container)
root.render(<App tab="home" />)

createRoot 启用React 18的并发功能。如果你不使用它,你的应用程序将表现得像在React 17上,你将无法体验到甜蜜的开箱即用的优化。所以现在,如果你还在使用 ,而不是 ,你会看到一个弃用通知。render createRoot

这是一个实验的好机会,看看新的并发功能是否能改善你的生产性能。你可以运行一个实验,其中一个变体有render ,另一个使用createRoot 。另外,你不会因为切换到新的API而破坏你的代码。你可以逐渐切换到createRoot ,而不会破坏你的应用程序。

为了确保你正确地迁移你的应用程序,尝试启用严格模式。严格模式会让你知道开发中的组件发生了什么,它会在控制台中打印出任何不正常的情况。启用严格模式不会影响生产构建。你可以在你的应用程序的某个地方这样做:

import React from "react"
import { createRoot } from "react-dom/client"

function App() {
  return (
    <div>
      <Header />
      <React.StrictMode>
        <div>
          <Content />
          <SignUpForm />
        </div>
      </React.StrictMode>
      <Footer />
    </div>
  )
}

const container = document.getElementById("app")
const root = createRoot(container)
root.render(<App />)

另外,如果你正在使用hydrate ,用于服务器端渲染的水合,你可以升级到hydrateRoot

// Before
import { hydrate } from "react-dom"
const container = document.getElementById("app")
hydrate(<App tab="home" />, container)

// After
import { hydrateRoot } from "react-dom/client"
const container = document.getElementById("app")
const root = hydrateRoot(container, <App tab="home" />)
// Unlike with createRoot, you don't need a separate root.render() call here.

就高级功能而言,就是这样。你可以看看React 18的其他突破性变化

让我们在下一节看看React 18带来了哪些新的好处。

React 18中的自动批处理

React 18给我们带来了自动批处理。这听起来可能令人困惑--你可能会问:"什么批处理?"。我们将通过它,不要担心。让我们看看一个例子:

// Before: only React events were batched
setTimeout(() => {
  setSize((oldSize) => oldSize + 1)
  setOpen((oldOpen) => !oldOpen)
  // React will render twice, once for each state update (no batching)
}, 1000)

// After: updates inside of timeouts, promises,
// native event handlers or any other event are batched
setTimeout(() => {
  setSize((oldSize) => oldSize + 1)
  setOpen((oldOpen) => !oldOpen)
  // React will only re-render once at the end (that is batching)
}, 1000)

自动批处理意味着React现在会对你在组件中的更新进行批处理。批次可以防止你的组件出现不必要的渲染。

在React 17中,如果你改变了组件的状态两次,该组件将重新渲染两次。现在,在React 18中,这两次更新将被分批进行,而组件将只渲染一次。而且,这只是在你使用createRoot ,而不是render 。 看看下面的例子吧:

如果自动批处理不是你想要的组件,你总是可以用flushSync 。让我们看一个例子:

import { flushSync } from "react-dom" // Note: we are importing from react-dom, not react

function handleSubmit() {
  flushSync(() => {
    setSize((oldSize) => oldSize + 1)
  })

  // React has updated the DOM by now
  flushSync(() => {
    setOpen((oldOpen) => !oldOpen)
  })

  // React has updated the DOM by now
}

setCountersetFlag 的调用将立即尝试更新DOM,而不是一起被批处理。

仅仅是这个新功能就可以使你的应用程序的性能发生变化。而且最酷的是,你只需要改变你的应用程序的安装点,就可以使用createRoot

让我们看看新版本中还有什么。

转场

React 18为过渡带来了新的API。过渡是React的一个新概念,用于区分紧急和非紧急更新:

  • 紧急更新是那些反映直接互动的更新,比如打字、点击、按压等等。
  • 过渡更新则是以非紧急的方式将UI从一个视图过渡到另一个视图。

让我们想象一个具有搜索功能的页面。一旦你向一个输入字段添加文本,你希望看到该文本立即显示在那里。这是一个紧急更新。但是,当你输入时,立即向用户显示搜索结果并不紧急。相反,在显示搜索结果之前,开发者通常会对用户的输入进行调试或节流。

因此,在一个输入字段中打字或点击一个过滤按钮是一个紧急的更新。显示搜索结果不是一个紧急更新,它被认为是一个过渡更新。让我们通过一个代码例子来看看:

import { startTransition } from "react"

// Urgent: Show what was typed in the input
setInputValue(newInputValue)

// Mark any state updates inside as transitions and mark them as non-urgent
startTransition(() => {
  // Transition: Show the results
  setSearchQuery(newInputValue)
})

被包裹在startTransition 中的更新被处理为非紧急的,如果有更紧急的更新,如点击或按键,则会被打断。假设一个过渡被用户打断了(例如,连续输入了多个字符)。在这种情况下,React会扔掉没有完成的陈旧的渲染工作,只渲染最新的更新。

你可以使用一个叫做useTransition 的钩子来获得一个待定的标志,像这样:

function App() {
  const [isPending, startTransition] = useTransition()
  const [count, setCount] = useState(0)

  function handleClick() {
    startTransition(() => {
      setCount((oldCount) => oldCount + 1)
    })
  }

  return (
    <div>
      <span>Current count: {count}</span>

      {isPending && <Spinner />}

      <button onClick={handleClick}>Increment</button>
    </div>
  )
}

新版本还有其他钩子,但首先,让我们看看我们等了很久的东西--Suspense --被带到我们的服务器端渲染应用程序中。

服务器上的悬念

Suspense 现在可以在服务器上使用。以前,它在客户端可以通过使用 进行代码分割。但是现在,你可以在你的组件 "暂停 "的时候有一个某种占位符。让我们在代码中看到它。React.lazy

<Suspense fallback={<PageSkeleton />}>
  <RightColumn>
    <ProfileHeader />
  </RightColumn>
  <LeftColumn>
    <Suspense fallback={<LeftColumnSkeleton />}>
      <Comments />
      <Photos />
    </Suspense>
  </LeftColumn>
</Suspense>

Suspense 如果树上的任何一个组件 "暂停 "了,就会回到你给它的组件上。但是,一个组件 "暂停 "是什么意思?它可以意味着很多事情,然而,在每一种情况下,它都意味着该组件还没有准备好进行渲染--它可能是缺少数据或代码。

这对上面的代码例子意味着什么?如果一个组件暂停了,它上面最接近的Suspense 组件就会 "抓住 "它,不管中间有多少个组件。在上面的例子中,如果ProfileHeader 暂停,那么整个页面将被替换成PageSkeleton

然而,如果CommentsPhotos 暂停,它们都会被替换成LeftColumnSkeleton 。这让你可以根据你的视觉UI设计的粒度安全地添加和删除Suspense 的边界,而不用担心可能依赖于异步代码和数据的组件。

如果你使用Suspense ,服务器上的慢速渲染组件就不会再拖整个页面的后腿。在GitHub上关于SSR Suspense的详细讨论中阅读更多信息。

一扇门也已经为第三方数据获取库打开,以支持Suspense。一些GraphQL或REST库可以支持暂停组件直到请求完成。你可以为数据获取和Suspense运行你自己的特别解决方案,但目前不建议这样做。

React 18中的5个新钩子

在React 18中,我们有五个新钩子:

1.useId

useId 是一个新的钩子,用于在客户端和服务器上生成唯一的ID,同时避免了水化不匹配比如说:

function CodeOfConductField() {
  const id = useId()

  return (
    <>
      <label htmlFor={id}>Do you agree with our Code of Conduct?</label>
      <input id={id} type="checkbox" name="coc" />
    </>
  )
}

2.useTransition

我们在上一节关于转换的内容中已经介绍了这个。

3.useDeferredValue

useDeferredValue 让你推迟重新渲染树的一个非紧急部分。它类似于debouncing或throttling,但有几个优点。没有固定的时间延迟,所以React会在第一次渲染反映在屏幕上后立即尝试延迟渲染。延迟渲染是可中断的,不会阻止用户输入。

如果我们看一下搜索的例子,我们就需要对使用延迟值的子组件进行记忆,让我们看一个例子:

function SearchResults() {
  const query = useSearchQuery("")
  const deferredQuery = useDeferredValue(query)

  // Memoizing tells React to only re-render when deferredQuery changes,
  // not when query changes.
  const suggestionResuls = useMemo(
    () => <SearchSuggestions query={deferredQuery} />,
    [deferredQuery]
  )

  return (
    <>
      <SearchInput query={query} />
      <Suspense fallback="Loading suggestion results...">
        {suggestionResuls}
      </Suspense>
    </>
  )
}

现在,SearchSuggestions 组件将重新渲染,只有当deferredQuery 被更新。为了把一切联系起来,当SearchSuggestions 被暂停时,我们会看到 "加载结果... "的文字。

4.useSyncExternalStore

useSyncExternalStore 是一个用于读取和订阅外部数据源的钩子,它与并发渲染功能(如选择性水化和时间切片)兼容。

这个钩子是为库的作者准备的,通常不在应用程序代码中使用。如果你正在维护一个库,而且听起来你可能需要它,你可以useSyncExternalStore 官方文档中阅读更多内容

5.useInsertionEffect

useInsertionEffect 的签名与useEffect 相同,但它所有 DOM 变异之前同步触发。这个钩子是为了在读取useLayoutEffect 中的布局之前将样式注入到DOM中。 它不能访问参考文献,也不能安排更新。

useInsertionEffect 这个钩子只适用于 库的作者。你应该使用 或 。css-in-js useEffect useLayoutEffect

如果你是css-in-js 库的作者或维护者,你可以在其文档中找到关于useInsertionEffect 的更多信息

其他值得注意的React 18变化

告别老式浏览器!

React 现在依赖于现代浏览器的功能,包括Promise,Symbol, 和Object.assign

如果你支持旧的浏览器和设备(如Internet Explorer),考虑在你的捆绑应用程序中包括一个全局polyfill,因为这些浏览器和设备没有原生提供现代浏览器功能,或者有不符合要求的实现。

组件现在可以渲染了undefined

如果你从一个组件中返回undefined ,React不再抛出一个错误。允许组件返回的值与组件树中间的允许值一致。React团队建议使用linter来防止错误,比如在JJSX之前忘记返回语句。

在未安装的组件上没有setState 警告

以前,当你在一个未挂载的组件上调用setState ,React会警告内存泄漏。这个警告是为订阅添加的,但人们主要是在设置状态没有问题的情况下遇到的,而变通方法会使代码恶化。

改进了内存使用情况

React现在在卸载时清理了更多的内部字段,所以在你的应用程序代码中,未修复的内存泄漏所带来的影响就不那么严重。看看与以前的版本相比,内存使用量是如何下降的,这很有意思。

总结:React 18带来了巨大的改进

关于React 18,React团队发布了很多新的、令人兴奋的消息。总结一下,这里有一个概述:

  • React.render 会警告你,你应该把它换成React.createRoot
  • ReactDOM.hydrate 会告诉你同样的事情React.hydrateRoot
  • 自动批处理是对状态更新进行批处理并一起执行,从而减少重新渲染的次数。
  • 过渡让你做更关键的状态更新,并可能中断其他非紧急的更新。该API是useTransitionstartTransition
  • 暂停允许你以一种不阻碍其他组件的方式对你的组件进行SSR。
  • 暂停也为数据框架打开了一条道路,使其能够进入并建立在它之上。这样一来,用数据框架获取的数据就会使组件开箱即用。
  • 几个新的钩子已经进来拯救了这一天。如果你决定使用useDeferredValue ,你的代码中可能不需要debouncethrottle
  • 旧的浏览器会受到影响,所以如果你需要支持它们,一定要添加polyfills。

就这样吧!我们已经看完了所有的主要变化。你可以在GitHub上阅读完整的React 18变更日志。什么变化最让你兴奋?

谢谢你的阅读,下一篇见。