前言:最近在忙于项目,又接了一个谷歌浏览器插件开发的任务,一直没有时间看新的技术更新,突然发现react已经更新到了18,随着react18正式版本发布,我们可以使用新版带来的新特性,快来一起看看都提供了哪些新的特性供我们使用吧。
注意:学习用例使用vite + react18搭建项目
一、初始化项目
1、初始化一个项目
npm init -y
2、安装react和react-dom
npm install react react-dom --save
3、安装
vite
和可以react热更新的
@vitejs/plugin-react-refresh
插件
vite
@vitejs/plugin-react-refresh
npm install vite @vitejs/plugin-react-refresh --save-dev
4、配置@vitejs/plugin-react-refresh插件,在项目中新建
vite.config.js
文件
vite.config.js
import { defineConfig } from "vite";
import reactRefresh from "@vitejs/plugin-react-refresh";
export default defineConfig({
plugins: [reactRefresh()],
});
5、package.json设置启动命令
"scripts": {
"start": "vite"
},
默认会找当前目录下index.html文件,因此,需要创建index.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vite-react18</title>
</head>
<body>
<div id="root"></div>
<script src="./main.jsx" type="module"></script>
</body>
</html>
6、根据路径新建main.jsx入口文件。
- react18之前写法
import React from 'react';
import {render} from 'react-dom';
const root = document.querySelector('#root');
const element = <h1>测试</h1>;
render(element, root);
此时启动项目
npm run vite
会看到控制台报错信息,react18不让继续使用此方法。
- react18写法:
import React from 'react';
import {createRoot} from 'react-dom/client';
const root = document.querySelector('#root');
const element = <h1>测试</h1>;
createRoot(root).render(element);
二、批量更新
- 在react中多次的setState合并到一次进行渲染。
- 在react18中更新是以优先级为依据进行合并。
1、旧版本react以前合并更新:
- 合并更新演示代码:
import React, { Component } from "react";
export default class OldBatchUpdate extends Component {
state = { age: 0 };
handleClick = () => {
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 0
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 0
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 0
setTimeout(() => {
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 2
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 3
});
};
render() {
return (
<div>
<span>{this.state.age}</span>
<button onClick={this.handleClick}>+</button>
</div>
);
}
}
- react18以前版本合并更新原理代码:
let state = { age: 0 };
let isBatchUpdate = false; // 批量更新标识
const updaeQueue =[]; // 批量更新队列
function setState(newState){
if(isBatchUpdate){
updaeQueue.push(newState);
}else{
state = newState;
}
}
function handleClick() {
setState({ age: state.age + 1 });
console.log(state.age);
setState({ age: state.age + 1 });
console.log(state.age);
setState({ age: state.age + 1 });
console.log(state.age);
setTimeout(() => {
setState({ age: state.age + 1 });
console.log(state.age);
setState({ age: state.age + 1 });
console.log(state.age);
});
}
// 更新函数
function batchUpdate(fn){
isBatchUpdate= true; // 启用批量更新
fn();
isBatchUpdate = false; // 关闭批量更新,此时同步任务执行结束,异步任务还没开始,所以,进入异步任务后,批量更新标识为false,也就是关闭了批量更新。
console.log(updaeQueue, 'updaeQueue')
updaeQueue.forEach(item=>{
state = item;
});
updaeQueue.length = 0;
}
batchUpdate(handleClick);
2、react18中新的更新机制
- 演示代码:
import React, { Component } from "react";
export default class BatchUpdate extends Component {
state = { age: 0 };
handleClick = () => {
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 0
this.setState({ age: this.state.age + 1 });
console.log(this.state.age);// 0
setTimeout(() => {
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 1
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 1
});
};
render() {
return (
<div>
<span>{this.state.age}</span>
<button onClick={this.handleClick}>+</button>
</div>
);
}
}
注意:可见,不论是在合成事件中,还是在宏任务中,都是会合并更新
const updaeQueue = [];// 更新队列
let onePriority = 1; // 更新优先级1,数字越小更新优先级越高
let towPriority = 2; // 更新优先级2
let larstPriority; // 上一次更新优先级
let state = { age: 0 }; // 初始化状态
function setState(newState, priority) {
updaeQueue.push(newState);
if (priority === larstPriority) {
return;
}
larstPriority = priority;
setTimeout(() => { // 模拟热更新
updaeQueue.forEach((item) => {
state = item;
});
updaeQueue.length =0;
});
}
// 新版更新不再依靠是合成事件或者宏任务微任务作为区分,而是,根据更新优先级来处理。
function handleClick() {
setState({ age: state.age + 1 }, onePriority);
console.log(state.age);// 0
setState({ age: state.age + 1 }, onePriority);
console.log(state.age); // 0
setTimeout(() => {
setState({ age: state.age + 1 }, towPriority);
console.log(state.age); // 1
setState({ age: state.age + 1 }, towPriority);
console.log(state.age); // 1
});
}
handleClick()
三、Suspense
- Suspense让你的组件在渲染完成之前进行等待,等待期间显示fallback中的内容。
- Suspense内的组件子树比其他组件数优先级更低。
- 完全同步写法,没有任何异步callback之类东西。
1、Suspense执行流程
- 在render函数中我们可以使用异步请求数据,而不使用await或者promise。
- react会从缓存中读取这个请求数据的promise。
- 如果没有请求完成就抛出一个promise异常。
- 当这个promise完成后(数据请求完成),react会重新回到原来的render中,将请求回来数据加载出来
2、Suspense子组件中直接调用promise举例:
import React, { Suspense, lazy } from "react";
const Home = lazy(() => import("../Home"));
import ErrorBoundary from "../ErrorBoundary";
export default function SuspensePage() {
return (
<Suspense fallback={<div>加载中。。。</div>}>
<ErrorBoundary>
<Home />
</ErrorBoundary>
</Suspense>
);
}
其中Home组件如下:
import React from 'react';
import {login} from '/src/services'
import { wrapPromise } from '../../utils';
const myLogin = login();
const loginRes = wrapPromise(myLogin);
export default function Home(){
const logins = loginRes.read(); // 此处直接调用promise,没有使用await或者then
return <div>返回结果:{logins.success?"成功":'失败'}, 请求返回信息: {logins.message}</div>
}
3、wrapPromise代码要遵顼Suspense流程
export function wrapPromise(promise) {
let status = "pending";
let result;
const subspender = promise
.then((resolve) => {
result = resolve;
status = "success";
console.log(resolve, "success");
})
.catch((error) => {
console.log(error, "err");
status = "error";
result = error;
});
return {
read() {
if (status === "pending") {
throw subspender;
} else if (status === "error") {
throw result;
} else if (status === "success") {
return result;
}
},
};
}
4、ErrorBoundary组件
import React, {Component} from 'react';
class ErrorBoundary extends Component{
state = {hasError: false, error: null};
static getDerivedStateFromError(err){ // 用于报错时候ui切换
return {hasError: true, error: err}
}
componentDidCatch(error, info){ // 用于上报错误信息
console.log(error, info);
}
render(){
if(this.state.hasError){
return <div>报错{this.state.error.toString()}</div>
}
return this.props.children;
}
}
export default ErrorBoundary;
5、Suspense原理
import React, { Component } from "react";
class Suspense extends Component {
state = { loading: false };
componentDidCatch(error) {
if (typeof error.then === "function") {
this.setState({ loading: true });
error.then(() => {
this.setState({ loading: false });
});
}
}
render() {
const { loading = false } = this.state;
const { fallback, children } = this.props;
if (loading) {
return <div>{fallback}</div>;
}
return children;
}
}
export default Suspense;
四、 startTransition
1、并发更新
- 并发更新就是可以中断渲染的架构。
- 什么时候中断渲染呢?当有更高级别渲染到来时,先放弃当前正在渲染的东西,而是,去立即执行更高级别的渲染,换来视觉上更快的相应速度。
- 在react18中以是否使用并发特性,作为是否开启并发更新的依据。
2、更新优先级
- react18以前没有更新优先级的概念,所有更新都要排队,不管优先级高不高,都要等待上一个更新执行结束才能执行。
-
react18为什么又要有更新优先级呢?用户对于不同的操作对交互的执行速度有着不同的预期。所以,我们可以根据用户的预期赋予不同的优先级。
- 高优先级:用户输入,窗口缩放,窗口拖拽。
- 低优先级:数据请求和文件下载。
- 高优先级更新会中断低优先级更新,等高优先级执行完以后,低优先级更新会根据高优先级执行结果重新更新。
- 对于cpu-bound的更新(例如:创建新的DOM节点),并发意味着一个更急迫的渲染可以中断已经开始的更新。
3、开启过渡更新(startTransition)
在输入框搜索东西的使用场景下,输入框优先级要比联想词高,所以,可以对联想词设置开启过渡更新。使用方法就是set值外包裹一层。
const [word, setWord]= useState([]);
startTransition(()=>{
setWord(new Array(10000).fill(1));
})
如果不开启过渡更新就会在输入内容时候卡死。开启后,会很流畅。
import React, { startTransition, useState, useEffect } from "react";
function AssociativeWord({ word }) {
const [wordList, setWordList] = useState([]);
useEffect(() => {
if (word.length > 0) {
startTransition(()=>{
setWordList(new Array(20000).fill(word));
})
}
}, [word]);
return (
<ul>
{wordList.map((item, index) => {
return <li key={index.toString()}>{item}</li>;
})}
</ul>
);
}
export default function StartTransitionPage() {
const [word, setWord] = useState("");
function handleInput(event) {
const {
target: { value = "" },
} = event;
setWord(value);
}
return (
<div>
<label htmlFor="world">请输入需要搜素的关键词:</label>
<input type="text" id="world" onChange={handleInput} />
<AssociativeWord word={word} />
</div>
);
}
注意:低优先级不会被丢弃,只是会在高优先级执行后执行
4、更新优先级问题
import React, {startTransition, useState, useEffect} from 'react';
export default function UpdatePriority(){
const [result, setResult] = useState('');
useEffect(()=>{
console.log(result, 'result');
}, [result]);
function handleChangeResult(){
setResult(item=>item + 'A');
startTransition(()=>{setResult(item=>item + 'B')});
setResult(item=>item + 'C');
startTransition(()=>{setResult(item=>item + 'D')});
}
return <div>
<h1>结果:{result}</h1>
<button onClick={handleChangeResult}>改变结果</button>
</div>
}
结果在控制台输出如下内容:
AC result
ABCD result
5、结论:
-
每次渲染时候会有一个渲染优先级,找到优先级最高的作为渲染优先级。
-
虽然优先级不同,但是,最终的结果顺序和调用顺序是一致的。
-
因为,优先级高的已经渲染过,会有diff比对更新,所以,相当于缓存。但最终还是都要渲染的。当执行到最低优先级时候,按照代码顺序全部执行一次。官方解释是类似于git,在master分支拉取A分支和B分支,但是,正在开发时候遇到master有bug,拉取一个hotfix分支C进行修改,C改完发布了,此时,发布A时候,C代码也会在里边。
五、 useDeferredValue
1、解决的问题
如果某些渲染比消耗性能,比如:实时计算和反馈,我们可以使用useDeferredValue来降低计算优先级,从而提升整体的性能。和startTransition作用类似,只是用法不同。
2、和startTransition的区别
- startTransition在目的组件中使用,包裹一层setValue来改变计算优先级,从而提升性能。
- useDeferredValue在源头改变,通过延时改变传入子组件值,降低优先级,提高性能。
- 一个在源头解决问题,一个在目的地解决问题。
3、使用方法:
import React, { useDeferredValue, useState, useEffect } from "react";
function AssociativeWord({ word }) {
const [wordList, setWordList] = useState([]);
useEffect(() => {
if (word.length > 0) {
setWordList(new Array(20000).fill(word));
}
}, [word]);
return (
<ul>
{wordList.map((item, index) => {
return <li key={index.toString()}>{item}</li>;
})}
</ul>
);
}
export default function UseDeferredValuePage() {
const [word, setWord] = useState("");
const defferedText = useDeferredValue(word);
function handleInput(event) {
const {
target: { value = "" },
} = event;
setWord(value);
}
return (
<div>
<label htmlFor="world">请输入需要搜素的关键词:</label>
<input type="text" id="world" onChange={handleInput} />
<AssociativeWord word={defferedText} />
</div>
);
}
六、useTransition
- 允许组件在切换到下一个组件之前等待加载内容,从而避免不必要的加载状态。
- useTransition返回两个值的数组。一个是isPending,另外一个是startTransition。
- 适用于加载很快的地方,将会使得Suspense中fallback不再执行,pending结束后直接渲染出来。
1、双缓冲
- 当数据量很大时候,绘图需要几秒或者更长的时间,而且,有时候会出现闪烁现象。为了解决这些问题,可采用双缓冲技术来绘图。
- 双缓冲即在内存中创建一个和屏幕绘图区域一致的对象,先将图像绘制到内存中这个对象上,再一次性将图形拷贝到界面上。这时候会加快绘图的速度。
2、useTransition使用
import React, { lazy, Suspense, useState, useTransition } from "react";
import ErrorBoundary from "../ErrorBoundary";
const Home = lazy(() => import("../Home"));
const DetailPage = lazy(() => import("../DetailPage"));
export default function UseTransitionPage() {
const [currentHome, setCurrentHome] = useState(true);
const [isPending, startTransition] = useTransition();
function handleChangePage() {
startTransition(() => { // 使用useTransition结构出来startTransition进行包裹
setCurrentHome(!currentHome);
});
}
return (
<Suspense fallback={<div>加载中。。。</div>}>
<ErrorBoundary>{currentHome ? <Home key="home"/> : <DetailPage key="detail" />}</ErrorBoundary>
<button onClick={handleChangePage}>切换</button>
{isPending?'切换中。。。':'已经切换'}
</Suspense>
);
}
以下 Hooks 是为库作者提供的,用于将库深度集成到 React 模型中,通常不用于应用程序代码。
七、 useSyncExternalStore
- 用来存储数据
import React, { useSyncExternalStore } from "react";
class Store {
value = 0;
listeners = [];
subscribe=(listener)=> {
this.listeners.push(listener);
}
getValue=()=> {
return this.value;
}
setValue=(newValue)=> {
this.value = newValue;
this.listeners.forEach((l) => l());
}
}
const store = new Store();
export default function UseSyncExternalStorePage() {
const state = useSyncExternalStore(store.subscribe, store.getValue);
function handleAdd() {
store.setValue(state + 1);
}
return (
<div>
<h2>{state}</h2>
<button onClick={handleAdd}>改变</button>
</div>
);
}
- 类似的store也可以结合换成redux。
import React, { useSyncExternalStore } from "react";
import { createStore } from "redux";
function reducer(state = { number: 0 }, action) {
switch (action.type) {
case "ADD":
return { ...state, number: state.number + 1 };
case 'SUB':
return {...state, number: state.number - 1};
default:
return state;
}
}
const store = createStore(reducer);
export default function UseSyncExternalStorePage() {
const state = useSyncExternalStore(store.subscribe, store.getState);
function handleAdd() {
store.dispatch({type: 'ADD'});
}
return (
<div>
<h2>{state.number}</h2>
<button onClick={handleAdd}>改变</button>
</div>
);
}
最新版本的react-redux使用useSyncExternalStore实现了redux和组件关联。
八、 useInsertionEffect
- 它在所有 DOM 突变*之前同步触发。*在读取useLayoutEffect之前触发, 由于此挂钩的范围有限,因此此挂钩无法访问 refs 并且无法安排更新。
-
useInsertionEffect
应该仅限于 css-in-js 库作者
九、学习参考