一些令人兴奋的新的改进已经随着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 。 看看下面的例子吧:
- ✅演示。React 18使用createRoot批处理,甚至在事件处理程序之外也是如此- 注意到控制台中每次点击都有一个渲染
- 🟡演示。React 18 with legacy render keeps the old behavior- notice two renders per click in the console.
如果自动批处理不是你想要的组件,你总是可以用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
}
对setCounter 和setFlag 的调用将立即尝试更新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 。
然而,如果Comments 或Photos 暂停,它们都会被替换成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.createRootReactDOM.hydrate会告诉你同样的事情React.hydrateRoot- 自动批处理是对状态更新进行批处理并一起执行,从而减少重新渲染的次数。
- 过渡让你做更关键的状态更新,并可能中断其他非紧急的更新。该API是
useTransition和startTransition。 - 暂停允许你以一种不阻碍其他组件的方式对你的组件进行SSR。
- 暂停也为数据框架打开了一条道路,使其能够进入并建立在它之上。这样一来,用数据框架获取的数据就会使组件开箱即用。
- 几个新的钩子已经进来拯救了这一天。如果你决定使用
useDeferredValue,你的代码中可能不需要debounce和throttle。 - 旧的浏览器会受到影响,所以如果你需要支持它们,一定要添加polyfills。
就这样吧!我们已经看完了所有的主要变化。你可以在GitHub上阅读完整的React 18变更日志。什么变化最让你兴奋?
谢谢你的阅读,下一篇见。