1.背景
(1)在react18中,增加了新的RootAPI `ReactDOM.createRoot()`,其相较于之前版本的`ReactDOM.render()`的用法,我们通过下面的🌰来对两种用法差别进行了解。
//1.ReactDOM.render()用法
ReactDOM.render(<App />,document.getElementById('root'));
// 2.ReactDOM.createRoot()用法
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
React18 中保留两种用法,老项目不想改动仍然可以使用 ReactDOM.render();新项目若想使用`并发渲染`便可以使用 ReactDOM.createRoot()。`并发渲染`就是我们本次介绍的主角。
(2)为了更好了解react 18与 之前版本区别,在了解新特性之前,我们先一起回顾下`setState`的相关内容。
- setState异步更新!
handleClick() {
this.setState({text: 'hi'});
console.log(this.state.text);//打印结果为未更新前的值
}
- setState异步更新?
在组件的生命周期或react合成事件中,setState为异步;而在setTimeout或原生DOM事件中,setState则为同步。
// setTimeout
handleClick() {
setTimeout(()=>{
this.setState({text: 'hi'});
console.log(this.state.text);//打印结果为hi
},0)
}
//原生DOM事件
componentDidMount() {
document.getElementById('btn').addEventListener('click',e=>{
this.setState({text: 'hi'});
console.log(this.state.text);//打印结果为hi
})
}
- setState合并
handleClick() {
//这里我们多次setState,实际count只改变一次
//react 执行setState时,会进行多次setState合并
this.setState({count: this.state.count+1});
this.setState({count: this.state.count+1});
this.setState({count: this.state.count+1});
}
2.Batch updating (批量更新)
上面对setState的相关回顾中,我们看到在 react 18 之前,react已经实现了对react 事件处理函数中的状态更新进行批量处理,但对于promise、setTimeout、原生事件处理函数中的状态更新默认是不会进行批量处理的;而react 18使用自动批量更新,使得在任何情况下都会实现多状态更新批量化处理。
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClickPrev() {
setCount((c) => c - 1); // 不会 re-render
setFlag((f) => !f); // 不会 re-render
// React 只会 re-render 一次
}
const fetchSomething = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("resolved");
}, 2000);
});
};
function handleClickNext() {
fetchSomething().then((data) => {
// React 17 及之前版本会触发两次重新渲染
// React 18 仅触发一次重新渲染
console.log(data);
setCount((c) => c + 1); // Causes a re-render
setFlag((f) => !f); // Causes a re-render
});
}
return (
<div>
<button onClick={handleClickPrev}>Prev</button>
<button onClick={handleClickNext}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
react17运行情况如下图,点击Next按钮,页面渲染两次:
react18运行情况如下图,点击Next按钮,仅触发一次渲染:
3. Transitions & Concurrent (过渡状态与并发渲染)
Concurrent rendering是一种可中断的渲染模式,当更高优先级渲染触发时,中断低优先级任务的渲染,执行高优先级任务的渲染,transitions 就是利用并发渲染来实现任务区分优先级的渲染方式,从而提高用户体验感。
在文章最初我们提到React 18 提供了新的 Root API,那么在react18中我们只需要使用ReactDOM.createRoot()这个新的Root API 便可以开启并发模式。但是值得注意的是,即便开启并发模式,也不一定启用并发特性,实现并发更新。接下来我们主要通过介绍transitions相关的几个API的用法来了解React 的并发特性。
● startTransition
在React更新中,任务本来并无优先级高低区分,但实际场景应用中,往往希望直接与用户交互的任务能够优先执行渲染,从用户感官层面提升页面流畅度。因此通常将用户交互行为如输入、点击等相关任务划分为优先级较高的任务,期望得到立即响应,而其他优先级较低的任务即使存在稍许延迟也不会给用户带来太差的体验。接下来我们以一个常见的应用场景为例进行分析。
// list.jsx
import { memo } from "react";
const mockData = new Array(5000).fill(0)
function Text({ query }) {
const text = 'abcdefghijk'
let children
if (!!query && text.includes(query)) {
const arr = text.split(query)
children = <div>{arr[0]}<span style={{ color: 'red' }} >{query}</span>{arr[1]} </div>
} else {
children = <div>{text}</div>
}
return <div>{children}</div>
}
/* 列表数据 */
function List({ query }) {
console.log('List渲染')
return (
<div>
{
mockData.map((item, index) => (
<div key={index} >
<Text query={query} />
</div>
))
}
</div>
)}
export default memo(List)
// app.jsx
import { useState, startTransition } from 'react'
import List from './list'
export default function App() {
const [value, setInputValue] = useState('')
const [query, setSearchQuery] = useState('')
const handleChange = (e) => {
setInputValue(e.target.value)
startTransition(() => {
setSearchQuery(e.target.value)
})
}
return (
<div>
<input
onChange={handleChange}
placeholder="输入搜索内容"
value={value}
/>
<List query={query} />
</div>
)
}
在demo中,针对一组5000条数据的列表进行渲染,并通过输入文本实现对搜索值的高亮显示,在这个场景中,用户交互的搜索框输入渲染部分是否流畅直接影响用户体验,该部分优先级相较于搜索结果渲染优先级更高;因此我们使用 startTransition 将搜索结果列表渲染任务标记为特殊更新类型 transitions 的非紧急任务,该任务可以被优先级更高的输入框更新中断,从而实现用户输入后得到更快响应,提升用户使用感受。
● useTransition
// useTransition 基本用法
// isPending 表示是否处于过渡状态; startTransition将任务标识为过渡任务
const [isPending, startTransition] = useTransition()
我们还以上面的场景为例,使用useTransition替换startTransition:
const [isPending, startTransition] = useTransition()
const handleChange = (e) => {
setInputValue(e.target.value)
startTransition(() => {
setSearchQuery(e.target.value)
})
}
return (
<div>
<input
onChange={handleChange}
placeholder="输入搜索内容"
value={value}
/>
{isPending ? <div>数据加载中...</div> :<List query={query} />}
</div>
)
上例中,我们通过isPending获取到过渡任务状态,在列表渲染处于过渡任务状态时,通过加载提示优化展示,提高用户体验感。
● useDeferredValue
与startTransition 一样,useDeferredValue本质上也是标识非紧急任务,而与startTransition将任务标识为非紧急任务不同的是,useDeferredValue是推迟到更紧急任务更新之后返回新的结果,使用效果与节流和防抖延迟更新类似,但优点是会在其他紧急任务执行后立即更新。
同样地,我们还以上述列表搜索为例介绍useDeferredValue的用法:
const query = useDeferredValue(value)
const handleChange = (e) => {
setInputValue(e.target.value)
}
console.log(value,query)
return (
<div>
<input
onChange={handleChange}
placeholder="输入搜索内容"
value={value}
/>
<List query={query} />
</div>
)
useDeferredValue使用渲染截图如下所示,从打印结果来看,query更新滞后于value的更新。
4.总结
本文简要介绍了React 18的几个新特性,有关React 18更新内容还有很多没介绍到的地方,请大家移步React官方文档查看最新内容,文中例子及内容有不正确的地方,欢迎大家指正。
5.参考资料
- https://react.docschina.org/blog/2022/03/29/react-v18.html
- https://react.dev/reference/react/useTransition#starttransition
- https://juejin.cn/post/7094037148088664078
- https://zhuanlan.zhihu.com/p/539322212
- https://juejin.cn/post/7027995169211285512