本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2022-06-18
支持react组件热更新的插件
import {defineConfig} from 'vite';
import reactRefresh from '@vitejs/plugin-react-refresh';
export default defineConfig({
plugins:[reactRefresh()]
});
// 旧模式 渲染是同步的
// ReactDOM.render(
// <Router>
// <ul>
// <li>
// <Link to="/BatchState">BatchState</Link>
// </li>
// <Routes>
// <Route path="/BatchState" element={<BatchState />} />
// </Routes>
// </ul>
// </Router>, document.getElementById('root'));
// 新模式 启用是并发渲染模式 会将优先级一样的更改合并到一起
const root = createRoot(document.getElementById('root')!);
root.render(
<Router>
<ul>
<li>
<Link to="/BatchState">BatchState</Link>
</li>
<Routes>
<Route path="/BatchState" element={<BatchState />} />
</Routes>
</ul>
</Router>
);
其表现就是
import React from 'react';
export default class extends React.Component {
state = {
number: 0,
}
// 开启了并发更新的打印 0 0 1 1
// 没有开启的打印 0 0 2 3
handleClick = () => {
this.setState({ number: this.state.number + 1 })
console.log(this.state);
this.setState({ number: this.state.number + 1 })
console.log(this.state);
setTimeout(() => {
this.setState({ number: this.state.number + 1 })
console.log(this.state);
this.setState({ number: this.state.number + 1 })
console.log(this.state);
})
}
render() {
return (
<div>
<p>
{this.state.number}
</p>
<button onClick={this.handleClick}>点击</button>
</div>
)
}
}
打印 0 0 2 3 的原因是在React18以前有这样一个机制,其表现如下:
let isBatchingUpdate = false;
let updateQueue = [];
let state = 0;
function setState(newState) {
if (isBatchingUpdate) {
updateQueue.push(newState);
} else {
state = newState;
}
}
function update() {
isBatchingUpdate = true;
setState(state+1);
setState(state+1);
setTimeout(() => {
setState(state+1);
setState(state+1);
})
isBatchingUpdate = false;
}
update();
state = updateQueue.pop();
console.log(state); // 1
setTimeout(() => {
console.log(state); // 3
}, 2000)
即同一个宏任务中的更新被合并到一起,以后面的覆盖前面的,下一个宏任务开始时,update执行完成了,但是对应这个update的更新任务,还有setTimeout中的两个,此时isBatchingUpdate已翻转,则直接更新。
使用
import React, { Suspense } from 'react';
interface CommonResponse {
success: boolean;
data: any;
}
function fetchUser(id: number): Promise<CommonResponse> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ success: true, data: { id, name: '姓名' + id }})
// reject({ success: false, error: '数据出错了'})
}, 1000)
})
}
function createResource(promise: Promise<any>) {
let status = 'pending';
let result: any;
return {
read() {
if (status === 'success' || status === 'error') {
return result
} else {
throw promise.then((data: any) => {
status = 'success';
result = data;
}, (err: any) => {
status = 'error';
result = err;
})
}
}
}
}
let userResource = createResource(fetchUser(1));
function User() {
let result = userResource.read();
if (result.success) {
let user = result.data;
return <div>{user.id} {user.name}</div>
}
throw result.error;
}
export default class extends React.Component {
render() {
return (
<Suspense fallback={<div>加载中...</div>}>
<User />
</Suspense>
)
}
}
大概原理:
loading ? fallback : children
,即在loading时显示占位的fallback否则显示Suspense的子组件。实现大致如下:
// MySuspense.tsx
import React from 'react';
interface Props {
fallback: React.ReactNode;
}
export default class extends React.Component<Props> {
state = { loading : false }
// render() 函数抛出错误,该函数可以捕捉到错误信息
// 子组件加载异常后也会被这个钩子捕获到
componentDidCatch(error: any) {
if (typeof error.then === 'function') { // 抛出的异常是Promise 置为true显示fallback
this.setState({ loading: true });
error.then(() => {
this.setState({ loading: false }) // 置为false后 显示对应的子组件
})
}
}
render() {
const { children, fallback } = this.props;
const { loading } = this.state;
return loading ? fallback : children;
}
}
import React, { Suspense, SuspenseList } from 'react';
function fetchUser(id: number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ success: true, data: { id, name: '姓名' + id } });
}, 1000*id);
});
}
// React.lazy
function createResource(promise: Promise<any>) {
let status = 'pending';//等待中,未知态
let result: any;
return {
read() {
if (status === 'success' || status === 'error') {
return result;
} else {
throw promise.then((data: any) => {
status = 'success';
result = data;
}, (error: any) => {
status = 'error';
result = error;
});
}
}
}
}
let user1Resource = createResource(fetchUser(1));
let user2Resource = createResource(fetchUser(2));
let user3Resource = createResource(fetchUser(3));
let userResourceMap:any = {
1:user1Resource,
2:user2Resource,
3:user3Resource
}
interface UserProps{
id:number
}
/**
* User组件它会依赖一个异步加载的数据
*/
function User(props:UserProps) {
let result: any = userResourceMap[props.id].read();
if (result.success) {
let user = result.data;
return <div>{user.id} {user.name}</div>
} else {
return null;
}
}
/**
* revealOrder 定义了SuspenseList子组件应该显示的顺序,默认是哪个先相应就先显示哪个
* together 在所有的子组件都准备好了的时候显示它们
* forwards 从前往后显示
* backwards 从后往前显示
* tail 指定如何显示SuspenseList尚未加载的项目
* 默认情况下,显示列表中所有fallback
* collapsed 仅显示列表中下一个fallback
* hidden 未加载的项目不显示任何信息
*/
export default class extends React.Component {
render() {
return (
<SuspenseList revealOrder="backwards" tail="collapsed">
<Suspense fallback={<div>3加载中....</div>}>
<User id={3} />
</Suspense>
<Suspense fallback={<div>2加载中....</div>}>
<User id={2} />
</Suspense>
<Suspense fallback={<div>1加载中....</div>}>
<User id={1} />
</Suspense>
</SuspenseList>
)
}
}
如果上面的User组件加载失败了(异步操作出错了)而不是暂时状态没更新,可以使用一个ErrorBoundary
组件借助getDerivedStateFromError
来进行一个友好的展示,因此我们可以将User中return null
改为 throw new Error('error')
来模拟这种场景,ErrorBoundary
的实现如下:
import React from 'react';
interface Props {
fallback:React.ReactNode
}
export default class extends React.Component<Props>{
state = {
hasError: false,
error: null
}
// 能从异步渲染中捕获错误
// 返回一个state,重置当前状态,并引发render重新渲染
static getDerivedStateFromError(error:any){
return {
hasError:true,
error
}
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
使用
export default class extends React.Component {
render() {
return (
<ErrorBoundary fallback={<div>出错了...</div>}>
<Suspense fallback={<div>加载中...</div>}>
<User />
</Suspense>
</ErrorBoundary>
)
}
}
下面实现的组件就是一个输入框,进行输入的时候,对应Suggestions
组件根据输入框展示一个列表。
import React, { startTransition, useEffect, useState } from 'react';
interface Props {
keyword: string;
}
function getWords(keyword:string){
let words = new Array(10000).fill(0).map((item:number,index:number)=> keyword+index);
return Promise.resolve(words);
}
function Suggestions(props: Props) {
let [words, setWords] = useState<Array<string>>([]);
useEffect(()=>{
getWords(props.keyword).then((words:Array<string>)=>{
// 开启渐变更新 本质就是低优先级的更新
startTransition(()=>setWords(words));
});
},[props.keyword]);
return (
<ul>
{
words.map((word: string) => <li key={word}>{word}</li>)
}
</ul>
)
}
export default function () {
const [keyword, setKeyword] = useState<string>('');
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setKeyword(event.target.value);
}
return (
<div>
关键字 <input value={keyword} onChange={handleChange} />
<Suggestions keyword={keyword} />
</div>
)
}
其本质是每次更新会产生一个update任务,而现在给update加上了优先级,优先级最高的最先执行。startTransition
即将update标注为最低优先级的更新。
官网的更新日志是这么说的:
startTransition 和 useTransition 让您将一些状态更新标记为不紧急。 默认情况下,其他状态更新被认为是紧急的。 React 将允许紧急状态更新(例如,更新文本输入)以中断非紧急状态更新(例如,呈现搜索结果列表)。
解决的是,当搜索结果有10000条,此时要渲染是需要花费时间的,而此时接着继续输入,会被“卡”住,因为10000条还没渲染完,此时用户得不到文本框的输入反馈。而使用这两个API后将可以做到优先处理用户的输入反馈,滞后处理过长搜索结果列表的反馈。
useTransition 允许组件在切换到下一个界面之前等待内容加载,从而避免不必要的加载状态。
它还允许组件将速度较慢的数据获取更新推迟到随后渲染,以便能立即渲染更重要的更新。
useTransition hook返回两个值的数组
如果某个state更新导致组件挂起,那么state更新应包装在transition中。
import React, { Suspense ,useState, useTransition} from 'react';
function fetchUser(id: number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ success: true, data: { id, name: '姓名' + id } });
//reject({ success: false, error: '数据加载失败' });
}, 5000);
});
}
//React.lazy
function createResource(promise: Promise<any>) {
let status = 'pending'; // 等待中,未知态
let result: any;
return {
read() {
if (status === 'success' || status === 'error') {
return result;
} else {
throw promise.then((data: any) => {
status = 'success';
result = data;
}, (error: any) => {
status = 'error';
result = error;
});
}
}
}
}
interface Props {
resource:any
}
// User第一次渲染的时候,调用read会抛出错误,被Suspense捕获到,从而展示fallback。
// 而同时这个错误是一个promise,当promise.then时代表异步处理完成,这时改变Suspense中的flag变量loading,翻转状态。
// 状态更新,组件重新渲染,此时调用read返回的result.success就是true了,即展示正常的User状态。
// 这个initialResource可以看出是一个将异步请求封装成能在加载前抛出一个promise错误的对象以便被Suspense捕获
function User(props:Props) {
let result: any = props.resource.read();
if (result.success) {
let user = result.data;
return <div>{user.id} {user.name}</div>
} else {
return <div>{result.error}</div>;
}
}
const initialResource = createResource(fetchUser(1));
export default function(){
const [resource, setResource] = useState(initialResource);
// 而使用useTransition能做到一个缓冲的效果
// 当用户点击按钮的时候,initialResource发生了改变
// User重新更新,此时又会抛出一个promise的错误对象,从而导致Suspense显示其fallback(加载中...),等加载完成之后再显示变化后的User信息
// useTransition则能优化这一部分,将处理数据变化的函数使用返回的startTransition包裹,也是低优先级更新的另一种展现,
// 即滞后展现时状态已经完成了,传递给User的resource一个是成功状态的了。所以不会有加载中那个展示的过程了。
// 数据变化,需要重新加载时,不会展示fallback(加载中...),而是等到状态完成之后,直接显示变化后的User信息
const [isPending, startTransition] = useTransition();
return (
<div>
<Suspense fallback={<div>加载中.....</div>}>
<User resource={resource}/>
</Suspense>
{/* isPending 用来处理如果等待状态完成的这个时间比较长的话,用户点击之后会感受到好像没有反应 */}
{/* 此时可以使用isPending的状态来给用户一个反馈,如果异步请求比较长的话 */}
{isPending ? <div>加载中....</div> : null }
<button
onClick={
()=>{
startTransition(()=>{
setResource(createResource(fetchUser(2)));
})
}
}
>
下一个用户
</button>
</div>
)
}
与startTransition相同的例子,只是将传递给Suggestions
的keyword使用useDeferredValue
包裹了一下,它的作用是内部会调用useState并触发一次更新,但此时更新的优先级很低,所以但用户不断输入的时候,deferredText
永远是“滞后”,类似于防抖效果,即Suggestions
不会实时接收到最新的值,而是输入值没有更新之后,“滞后”接收到的那个值。
useDeferredValue 允许您推迟重新渲染树的非紧急部分。它类似于去抖动,但与之相比有一些优点。没有固定的时间延迟,因此 React 将在第一次渲染反映在屏幕上后立即尝试延迟渲染。 延迟渲染是可中断的,不会阻止用户输入。
export default function () {
const [keyword, setKeyword] = useState<string>('');
const deferredText = useDeferredValue(keyword); // 不需要使用 startTransition 了
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setKeyword(event.target.value);
}
return (
<div>
关键字 <input value={keyword} onChange={handleChange} />
<Suggestions keyword={deferredText} />
</div>
)
}