简介
2022年3月29日 react18 正式发布
React18 最大的变更就是 出现新的一个概念 transition
,在 React18 中,引进了两个新的 API —— startTransition
、flushSync
; 还有三个新的 hooks —— useTransition
、useDeferredValue
、useId
;
React 17 和 React 18 最大的区别就其更新机制:由同步不可更新变成了异步可中断更新,React17 可以通过一些试验性的 API 开启并发模式,而 React18 则全面开启并发模式。
1、并发模式
什么是Transition?
transition
英文翻译为‘过渡’,那么这里的过渡指的就是在一次更新中,数据展现从无到有的过渡效果。用 ReactWg 中的一句话描述 startTransition ;
在大屏幕视图更新的时,startTransition 能够保持页面有响应,这个 api 能够把 React 更新标记成一个特殊的更新类型 transitions ,在这种特殊的更新下,React 能够保持视觉反馈和浏览器的正常响应。
我们试想一下:在一个大型项目中如果我们的页面中视图渲染假如有1w个组件,如果进行渲染工作视图更新,那么当你的设备很垃圾时,大量的渲染工作必将导致页面给用户的感受就是卡顿的,这是我们可以想想能不能让页面一些比较重要的组件先完成渲染更新,其他不重要的组件先不进行渲染优先级往后放一点呢?
这就是 Transition 的初衷,Transition 本质上是用于一些不是很急迫的更新上,把不重要的更新往后放放,降低一下优先级,让页面其他急迫的任务先执行;
于是我们会把状态分为两类:
- 第一类是紧急的更新任务:比如用户交互行为、按键、点击、输入等;
- 第二类就是过渡更新任务(transition):比如ui从一个视图过渡到另一个视图;
我们把这个模式叫做 concurrent Mode
并发模式,在这个模式下,渲染任务是可以中断的,我们在这个模式就可以去设置各个状态视图渲染更新的优先级,可以说 React18 就是围绕着更优质的用户体验去展开的;
如何开启并发模式?
React18 使用 ReactDOM.createRoot()
创建一个新的根元素进行渲染,使用该 API,会自动启用并发模式。如果你升级到 React18,但没有使用ReactDOM.createRoot()
代替ReactDOM.render()
时,控制台会打印错误日志要提醒你使用 React,该警告也意味此项变更没有造成 breaking change,而可以并存,当然尽量是不建议。
// react17
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(<App />, document.getElementById('root'))
// react18
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)
新API - startTransition
这是 React18 新增的一个方法,我们一般使用改方法开启过渡任务
startTransition(scope)
scope 是一个回调函数,里面的更新任务都会被标记成过渡更新任务,过渡更新任务在渲染并发场景下,会被降级更新优先级,中断更新。
const onDataChange = (data) => {
// 紧急的更新任务
setCount(data);
startTransition(() => {
// 过渡更新任务
setLowTask(true);
})
}
新Hook - useTransition
先思考一个问题:过渡更新任务是可以中断,先去处理其他优先级高的任务,再回来继续处理的,那么就有一个问题,过渡期状态怎么呈现?我们怎么知道当前正处于一个过渡期呢?
这个时候你就需要用到useTransition
,useTransition 执行返回一个数组。数组有两个状态值:
- 第一个是,当处于过渡状态的标志——isPending,当任务为悬停状态时它的值是
true
。 - 第二个是一个方法,可以理解为上述的 startTransition。可以把里面的更新任务变成过渡任务。
import { useState, useTransition } from 'react';
const Demo = () => {
const [ value ,setInputValue ] = useState('')
const [ query ,setSearchQuery ] = useState('')
const [ isPending , startTransition ] = useTransition()
const handleChange = (e) => {
setInputValue(e.target.value)
startTransition(()=>{
setSearchQuery(e.target.value)
})
}
return (
<div>
{ isPending && <span>isTransiton</span> }
<input
onChange={handleChange}
placeholder="输入搜索内容"
value={value}
/>
<NewList query={query} />
</div>
)
}
新Hook - useDeferredValue
useDeferredValue(value)
这个Hook接收一个参数,把原值(传入的参数)通过过渡任务得到新的值,这个值作为延时状态;
useDeferredValue 和上述 useTransition 本质上有什么异同呢?
相同点:
- useDeferredValue 本质上和内部实现与 useTransition 一样都是标记成了过渡更新任务。
不同点:
- useTransition 是把 startTransition 内部的更新任务变成了过渡任务transtion,而 useDeferredValue 是把原值通过过渡任务得到新的值,这个值作为延时状态。 一个是处理一段逻辑,另一个是生产一个新的状态。
- useDeferredValue 还有一个不同点就是这个任务,本质上在 useEffect 内部执行,而 useEffect 内部逻辑是异步执行的 ,所以它一定程度上更滞后于 useTransition。 useDeferredValue = useEffect + transtion
import { useState, useDeferredValue } from 'react';
const Demo = () => {
const [value ,setInputValue] = React.useState('')
const query = React.useDeferredValue(value)
const handleChange = (e) => {
setInputValue(e.target.value)
}
return
(
<div>
<button>useDeferredValue</button>
<input onChange={handleChange}
placeholder="输入搜索内容"
value={value}
/>
<NewList query={query} />
</div>
)
}
2、自动批处理
批处理是指 React 将多个状态更新,聚合到一次 render 中执行,以提升性能;
在 React17 的批处理只会在事件处理函数中实现,而在 Promise 链、异步代码、原生事件处理函数中失效。而 React18 则所有的更新都会自动进行批处理
// react17
const handleBatching = () => {
// re-render 一次,这就是批处理的作用
setCount((c) => c + 1)
setFlag((f) => !f)
}
// re-render两次
setTimeout(() => {
setCount((c) => c + 1)
setFlag((f) => !f)
}, 0)
// react18
const handleBatching = () => {
// re-render 一次
setCount((c) => c + 1)
setFlag((f) => !f)
}
// 自动批处理:re-render 一次
setTimeout(() => {
setCount((c) => c + 1)
setFlag((f) => !f)
}, 0)
当然如果你不想要批处理,可以使用flushSync方法取消批处理
const handleAutoBatching = () => {
// 退出批处理
flushSync(() => {
setCount((c) => c + 1)
})
flushSync(() => {
setFlag((f) => !f)
})
}
3、新Hook - useId
useId
支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration 的不匹配,原理就是每个 id 代表该组件在组件树中的层级结构。
function Checkbox() {
const id = useId()
return (
<>
<label htmlFor={id}>Do you like React?</label>
<input id={id} type="checkbox" name="react" />
</>
)
}