现在终于等到了 React 18
,所以我打算好好看看新版本的这些特性,到底香不香!
新版本新项目当然是要创建一个新的 TypeScript
的react
应用咯,这里为了方便直接使用官方脚手架了:
# 网上很多说这个,这个命令是不行的!!!
npx create-react-app react-ts-demo --typescript
# 使用此命令,正确
npx create-react-app react-ts-demo --template typescript
一切准备就绪,开卷(bushi)!
想必大家对 ReactDOM.render
API 都不会陌生吧,先上一段怀旧代码:
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
Hello, Eagle!
,
document.getElementById('root')
);
咦!?居然,可以正常执行,不是说废弃了吗,难道是我版本没整对?不要慌!
React
官方为了能让大家平稳的过渡并切换到 React 18
可以说也是用心良苦,当我执行上面这段代码就会悄悄地在控制台给我报个错,就像这样:
什么意思呢?我给大家翻译一下:还想用ReactDOM.render
是吧,滚去用 React 17
吧。createRoot
它不香吗!
下面就来看看 createRoot
咋用:
import React from 'react';
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
Hello, Eagle!
,
);
其实对比起来变化也不大:
ReactDOM
引用路径变成了 react-dom/client
;
需要单独再调用一下 render
方法在根节点上创建你自己的内容;
我们要用 React 18 的新特性一定要使用 createRoot 否则是无法享受到并发模式带来的好处的。
新来看一段老代码:
index.tsx
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render( , document.getElementById('root'));
App.tsx
import { useState } from 'react';
import './App.css';
function App() {
console.log('点 click me 之后,看我出现几次就知道渲染了几次。');
const [count, setCount] = useState(0);
const [msg, setMsg] = useState('hello');
function handleClick() {
console.log('点了');
setTimeout(() => {
setCount(c => c + 1);
setMsg('it\'s me');
}, 1000);
}
return (
flag: {msg}
count: {count}
);
}
export default App;
点击执行效果如下:
显然是渲染了两次!
现在我们把 index.tsx
改成新代码,App.tsx
不变:
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render( );
点击执行效果如下:
这回就渲染了一次!
其实我们在开发中这种异步(setTimeout、Promise 等,非异步逻辑里面不存在这个问题哈)逻辑里面去修改状态的情况很常见,过去都是你设置几次状态就会渲染几次,现在只需要一次,而且这个特性是默认开启的。
咦?那这个特性还能关?昂
我们改一下 App.tsx
中的代码:
import { useState } from 'react';
import { flushSync } from 'react-dom';
import './App.css';
function App() {
console.log('点 click me 之后,看我出现几次就知道渲染了几次。');
const [count, setCount] = useState(0);
const [msg, setMsg] = useState('hello');
function handleClick() {
setTimeout(() => {
flushSync(() => {
setCount(c => c + 1);
});
flushSync(() => {
setMsg('it\'s me');
});
}, 1000);
}
return (
flag: {msg}
count: {count}
);
}
export default App;
我去,果然关了,能多渲染几次是几次,不推荐,真的不推荐。
Transitions
也是并发模式提供的一个特性,主要提供了两个 API
:
useTransition
(hook 模式)
startTransition
(非 hook 模式)
Transitions
字面上的意思是过渡、转场,而 React 18
中的 Transitions
主要的作用是用来控制渲染的优先级,某一段逻辑被 Transitions API
包裹起来就相对于是给这段逻辑加了个过渡或者叫转场,那么这段逻辑自然就会稍缓一些执行了。
正常的 setState
类型的操作都是高优先级的,但是加了 Transitions API
包裹就会被降低优先级,这就是 Transitions
的主要作用。
是不是还挺 easy 的。
再 easy 还是得举个啊。
首先搞一下渲染很费劲的组件,渲染一次需要老长时间了,这个组件我就随便实现了一下,会根据父组件输入框的输入来渲染出输入文本长度 3000 倍(电脑性能差的小伙伴可以改小点儿,要不然分分钟卡死)的列表。
longtime.tsx
:
export default function Longtime(props: { liLen: number }) {
const liLen = props.liLen;
const creatData = () => {
const elements: number[] = [];
for (let i = 0; i < liLen * 3000; i++) {
elements.push(Math.random());
}
return elements;
};
return (
{
creatData().map((v, i) => {
return (- {v}
);
})
}
);
}
父组件还是 App.tsx
:
import React from 'react';
import { useState } from 'react';
import Longtime from './longtime';
function App() {
const [text, setText] = useState('');
const [liLen, setLiLen] = useState(0);
const handleChange = (e: React.FormEvent) => {
setText(e.currentTarget.value);
setLiLen(e.currentTarget.value.length);
}
return (
);
}
export default App;
这段代码执行真的非常的慢,我输入文本会感觉我并没有输进去一样,要过一段时间才会在 input 框里面有反馈(如果直接粘贴进去一段文本,比如:1234567,效果会更明显),这种体验真的是非常不好。
可以看一下下面这个动画的效果:
接下来该我们的 Transitions
出厂了,直接上代码:
修改一下 App.tsx
:
import React from 'react';
import { useState, useTransition } from 'react';
import Longtime from './longtime';
function App() {
const [isPending, startTransition] = useTransition();
const [text, setText] = useState('');
const [liLen, setLiLen] = useState(0);
const handleChange = (e: React.FormEvent) => {
setText(e.currentTarget.value);
startTransition(() => {
setLiLen(e.currentTarget.value.length);
});
}
return (
{isPending && Loading
}
{/* 写成下面这种效果会更明显 */}
{/* {isPending ? Loading
: } */}
);
}
export default App;
直接粘贴进去一串文本“1234567”,立马就出现在了 input 框里面,然后再慢慢渲染 Longtime
组件的内容,这样的体验确实会好很多。
总结:Transitions
可以区别渲染优先级,适合应对同时有大量耗时渲染的情况。
官方介绍:
useDeferredValue
接收一个值并返回该值的新副本,该副本值的渲染将会推迟到更紧急的更新之后。更紧急的更新一般就是和用户有交互的更新,比如:Input 框输入文字这样的,遇到这种情况,React
就会返回以前的值,然后去渲染想文本输入这样的紧急的更新。
这个 hook
的作用有点儿类似防抖和节流,他俩其实也是延迟更新。不同的是,延迟和防抖都有一个固定的时间,而 useDeferredValue
将会在其它工作完成之后里面去执行延迟的操作。
useDeferredValue
只延迟传递给它的值。如果要防止子组件在紧急更新期间重新渲染,还必须使用 useMemo
来记录值。
useDeferredValue
和 Transitions
有些相似,不同的是前者延迟的是值,后者延迟的是一段逻辑或者叫更新任务吧。
修改一下上面 Transitions
中的代码,实现相同的效果:
App.tsx
import React, { useState, useDeferredValue, useMemo } from 'react';
import Longtime from './longtime';
function App() {
const [text, setText] = useState('');
// 可以和这种直接复制的方式对比效果
// const liLen = text.length;
const liLen = useDeferredValue(text.length);
const handleChange = (e: React.FormEvent) => {
setText(e.currentTarget.value);
}
const longtime = useMemo(() => , [liLen]);
return (
{longtime}
);
}
export default App;
官方介绍是用于生成一个跨服务端和客户端的稳定的唯一的 ID,同时避免水合不匹配。貌似是和服务端渲染相关的东西,没太关注这块儿,也不太明白具体有啥用处,大家还是看 useId 的官方文档吧。
官方介绍这俩 hook
是为第三方库(比如:Redux
)作者提供的,用于将库深入集成到 React
模型中,通常不会在应用程序代码中使用。
看到这里我就打算先不看这俩 hook
了,我暂时用不到,大家有兴趣可以看看 官方文档。
Suspense
这个 API
其实最早是在 16.x 版本就支持了,但是之前没上并发模式支持得并不算完美,React 18
正式上线之后 Suspense
才支持得更完美,另外还有个原因就是,我之前也没好好学习这个 API
所以也算是补一下吧。
开发实战中,你是不是经常遇到这几种情况:
先要异步请求一些数据,然后再显示组件,在此之前需要显示 Loading 组件
先要异步请求一些数据,然后再显示组件A,组件A包含一个子组件B,子组件B又要先异步请求一些数据才能渲染出来,在此之前组件A 和 子组件B 都需要显示各种的 Loading 组件
遇到这些情况的时候我们通常就会写出这样的代码:
import { useEffect, useState } from 'react';
function stop(ms: number) {
return new Promise((resolve) => {
setTimeout(() => {
resolve('success');
}, ms);
});
}
function fetchSomething(url: string, callback: () => void) {
if (url === '/api1') {
stop(1000).then(() => {
callback();
});
} else if (url === '/api2') {
stop(2000).then(() => {
callback();
});
}
}
function Loading() {
return (
Loading...
);
}
function App() {
return (
);
}
function CompoentA() {
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchSomething('/api1', () => {
// ...省略一些逻辑
setLoading(false);
});
}, []);
return (
<>
我是组件A 的部分内容
{
loading ? :
}
>
);
}
function CompoentB() {
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchSomething('/api2', () => {
// ...省略一些逻辑
setLoading(false);
});
}, []);
return (
<>
我是组件B 的部分内容
{
loading ? : 我是组件B 异步加载的内容
}
>
);
}
export default App;
执行结果:
实际的情况可能会比这个复杂很多,那么上面的代码有什么问题呢:
ComponentA
和 ComponentB
都要异步请求数据,但是他俩要请求的数据其实没有什么相关性,完全可以同时请求,但是上面的写法会把两个请求变成串行,导致页面加载时间变长;
问题1我们通常可以通过 Promise.all
来优化数据请求(如果不用 Promise.all
页面可能会多次渲染),不过优化程度有限,还是要等待最慢那个请求,其它地方才能渲染,可以说优化并不够彻底,我就不在此实现了;
写法上也不够简洁,复杂的组件可能显得很乱;
Suspense
为我们提供了一种更友好的方案,以解决以上这类问题。它允许我们为尚未准备好的组件指定一个 fallback
(应急、后备)来解决这类问题。
Suspense
这个单词的本意有悬而未决
的意思,我们需要异步请求完数据才能加载的组件将是什么样子呢,确实也是悬而未决,得等请求返回啊,还挺贴切的。
下面用 Suspense
来改造一下上面的代码:
import { Suspense } from 'react';
function stop(ms: number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success');
}, ms);
});
}
function promiseWrapper(promise: Promise) {
let status = 'pending';
let result: any;
return {
read() {
if (status === 'pending') {
throw promise.then(
res => {
status = 'success';
result = res;
},
err => {
status = 'error';
result = err;
}
);
} else if (status === 'error') {
throw result;
} else if (status === 'success') {
return result;
}
}
};
}
function Loading() {
return (
Loading...
);
}
function App() {
const resourceA = promiseWrapper(stop(1000));
const resourceB = promiseWrapper(stop(2000));
return (
<>
}>
}>
>
// 下面是串行方式
// <>
// }>
//
// }>
//
//
//
// >
);
}
function CompoentA(props: any) {
const result = props.data.read();
if (result === 'success') {
return (
<>
我是组件A 的部分内容
>
);
} else {
return null;
}
}
function CompoentB(props: any) {
const result = props.data.read();
if (result === 'success') {
return (
<>
我是组件B 的部分内容
我是组件B 异步加载的内容
>
);
} else {
return (
<>
我是组件B 的部分内容
>
);
}
}
export default App;
执行效果:
上面这个代码执行效果还是不错的,不过看着还是有点儿复杂,不过值得一说的是,loading
状态变量省了,useEffect
也省了,光看两个组件其实代码算是清爽了不少,不过 promiseWrapper
是个上面鬼啊。
简单解释一下这里的原理:
在 Suspense
包裹的组件内部,如果 React
检测到一个被 throw
出来的 promise
,那 React
就会 catch
住它,并找到离它最近的 Suspense
组件,这样这个 Suspense
就知道它需要等这个 promise
完成,接着它就可以直接渲染 fallback
部分的组件了,在 promise
变成 resolve
状态后就会渲染包裹在其内部的组件了。
另外,promiseWrapper
的代码也正是 React.lazy
这个 API
实现的原理。
所以为了使用 Suspense
数据请求的方法是需要符合 promiseWrapper
这种形式的约定的。
目前,我们经常用的 fetch
、axios
等发请求常用的库似乎都不支持上述的约定,都需要自己使用的时候去适配一下,不过 SWR 是支持的,推荐大家使用。
SuspenseList
这个 API
最早也是在 16.x 版本就支持了,这个 API
是用来控制多个 Suspense
的显示顺序的,既然学了 Suspense
,顺便也把他一学吧。
我尝试去引入 SuspenseList
:
import { Suspense, SuspenseList } from 'react';
结果:
网上查了半天都说是这样用的,直到看到 官方 issue 我才死心了,React 18.0.0
不再导出 SuspenseList
了,被放到了 @experimental
版本了,只能等后面的 18.x 版本了。
差不多就写到这儿吧,暂时是够用了,欢迎各位大佬一起探讨,不吝赐教。