React 18 相对于 React 17 的主要升级内容
1. 并发特性(Concurrent Features)
React 18 引入了并发特性,使得 React 能够在后台准备多个版本的 UI,从而提升应用的响应速度和用户体验。
- 并发渲染(Concurrent Rendering) :允许 React 在后台准备多个版本的 UI,并在合适的时候应用这些更新。
useTransition
和startTransition
:这些 API 用于处理 UI 过渡状态,允许你将某些状态更新标记为低优先级,从而保持 UI 的响应性。useDeferredValue
:用于延迟更新某些状态,避免频繁的重新渲染,从而提升性能。
useTransition
useTransition
是 React 18 中新增的一个 Hook。它主要用于处理 UI 中的过渡状态,特别是在需要处理用户输入或者其他需要响应的操作时,可以让 React 在后台处理一些状态更新,从而避免 UI 的卡顿。
useTransition
返回一个布尔值和一个函数。布尔值表示当前是否处于过渡状态,函数用于启动过渡状态更新。
const [isPending, startTransition] = useTransition();
isPending
:一个布尔值,表示当前是否有过渡中的状态更新。startTransition
:一个函数,用于启动过渡状态更新。
下面是一个使用 useTransition
的示例,展示如何在处理大量数据时保持 UI 的响应性。
import React, { useState, useTransition } from 'react';
const App = () => {
const [isPending, startTransition] = useTransition();
const [input, setInput] = useState('');
const [list, setList] = useState([]);
const handleChange = (e) => {
const value = e.target.value;
setInput(value);
startTransition(() => {
const newList = Array(10000)
.fill(0)
.map((_, i) => `${value} ${i}`);
setList(newList);
});
};
return (
<div>
<input type="text" value={input} onChange={handleChange} />
{isPending ? <p>Loading...</p> : null}
<ul>
{list.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
};
export default App;
通过使用 useTransition
,我们可以将一些计算量大的状态更新操作放入过渡状态,允许你将某些状态更新标记为低优先级,从而保持 UI 的响应性。React 会在后台处理这些过渡状态更新,使得用户体验更加流畅。
useDeferredValue
useDeferredValue
允许你将某个值的更新标记为低优先级,从而确保高优先级的更新(例如用户输入)能够快速响应。具体来说,它会返回一个延迟更新的值,这个值会在高优先级的更新完成后再进行更新。主要用于处理高优先级的更新和低优先级的更新之间的关系。它可以帮助你在用户交互(例如输入)和复杂计算或渲染之间建立平衡,从而提高应用的性能和响应速度。
使用场景
- 用户输入:当用户在输入框中输入内容时,你希望输入框能够立即响应用户的输入,而不是被复杂的计算或渲染所阻塞。
- 复杂计算:当某个计算或渲染非常耗时时,你可以将其结果的更新标记为低优先级,以确保用户的交互操作不会被阻塞。
示例代码
假设我们有一个搜索框,当用户输入内容时,我们需要显示搜索结果。搜索结果的计算可能比较耗时,我们希望用户输入时输入框能够立即响应,而不是被搜索结果的计算所阻塞。
当我们快输入12345时,如果没有使用deferredQuery
,输入框的内容变化响应会感觉到卡顿。
import React, { useState, useDeferredValue, useMemo } from 'react';
// 假数据
const data = Array.from({ length: 10000 }, (_, index) => `Item ${index + 1}`);
function SearchComponent() {
const [query, setQuery] = useState('');
// 过滤数据的操作被推迟,以确保输入框的快速响应
const filteredData = useMemo(() => {
return data.filter(item => item.toLowerCase().includes(query.toLowerCase()));
}, [query]);
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>
{filteredData.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
export default SearchComponent;
当我们快输入12345时,使用deferredQuery
,会感觉到输入框的内容变化响应会先于列表的筛选结果
import React, { useState, useDeferredValue, useMemo } from 'react';
// 假数据
const data = Array.from({ length: 10000 }, (_, index) => `Item ${index + 1}`);
function SearchComponent() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// 过滤数据的操作被推迟,以确保输入框的快速响应
const filteredData = useMemo(() => {
return data.filter(item => item.toLowerCase().includes(deferredQuery.toLowerCase()));
}, [deferredQuery]);
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>
{filteredData.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
export default SearchComponent;
2. 自动批处理(Automatic Batching)
React 18 引入了自动批处理更新的功能,即使是在异步事件中,多个状态更新也会被批处理在一起,从而减少不必要的重新渲染。
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
function handleClick() {
setCount(c => c + 1);
setText('Updated');
// 在 React 18 中,这两个更新会自动批处理在一起
}
return (
<div>
<button onClick={handleClick}>Update</button>
<p>{count}</p>
<p>{text}</p>
</div>
);
}
react17不会自动批处理吗?
在 React 17 中,自动批处理(Automatic Batching)仅限于 React 事件处理程序内的状态更新。但是在其他异步操作(如 setTimeout
、Promise
或者原生事件处理程序)中,React 17 并不会自动批处理状态更新。
从 React 18 开始,自动批处理的范围被扩展到了所有的异步操作,包括 setTimeout
、Promise
、原生事件处理程序等。
例如以下代码,如果点击按钮,react17会log两次,而react18只会log一次
import React, { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
console.log('-------------------------------------', new Date().getTime());
const handleClick = () => {
setTimeout(() => {
setCount(count + 1);
setText('Updated');
// 这两个状态更新不会被批处理,导致两次重新渲染
}, 1000);
};
return (
<div>
<p>Count: {count}</p>
<p>Text: {text}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
export default App;
在 React 17 中,如果需要在异步操作中手动批处理状态更新,可以使用 unstable_batchedUpdates
函数。
例如以下代码,如果点击按钮,react17只会log一次
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { unstable_batchedUpdates } from 'react-dom';
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const handleClick = () => {
setTimeout(() => {
unstable_batchedUpdates(() => {
setCount(count + 1);
setText('Updated');
// 这两个状态更新会被批处理,导致一次重新渲染
});
}, 1000);
};
return (
<div>
<p>Count: {count}</p>
<p>Text: {text}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
export default App;
React 中 setState 是同步还是异步的?
✅ setState
本质上是同步 的,但 React 18 之前 在合成事件和生命周期方法中进行了批处理,使其表现为异步。
✅ 在 原生事件、setTimeout、Promise 中,setState
通常是同步的,但 React 18 之后也支持批处理。
✅ 不要依赖 this.state
获取最新值,正确的方式是使用回调函数
this.setState((prevState) => ({ count: prevState.count + 1 }));
react18之前
react18之前都是使用render进行渲染,在 合成事件 和 生命周期方法 中,setState
是异步的,React 会批量更新状态;但是在 原生事件 或 setTimeout
里,setState
是同步的。
import { render } from 'react-dom';
render(<App />, document.getElementById('root'));
react18
react18使用createRoot进行渲染,无论在 合成事件 和 生命周期方法 中,还是在 原生事件 或 setTimeout
里,setState
都是异步的
import { createRoot } from 'react-dom/client';
createRoot(root).render(<App />);
3. useId
Hook
React 18 引入了 useId
Hook,通过使用 useId
,我们可以确保生成的 ID 是唯一且稳定的,避免 ID 冲突,并在服务器端渲染和客户端渲染之间保持一致性。
1、表单元素的关联
在表单中,我们通常需要为每个输入元素生成一个唯一的 ID,以便 label
标签能够正确地关联到相应的输入元素。使用 useId
可以简化这个过程,并确保生成的 ID 是唯一且稳定的
import { useId } from 'react';
function MyComponent() {
const id = useId();
return (
<div>
<label htmlFor={id}>Enter your name:</label>
<input id={id} type="text" />
</div>
);
}
2、动态生成表单项
假设我们有一个动态生成的表单,其中的表单项可以根据用户的输入动态增加或减少。使用 useId
可以确保每个动态生成的表单项都有一个唯一的 ID。
import React, { useState, useId } from 'react';
function DynamicForm() {
const [fields, setFields] = useState([{ id: useId(), value: '' }]);
const addField = () => {
setFields([...fields, { id: useId(), value: '' }]);
};
const handleChange = (id, event) => {
const newFields = fields.map(field =>
field.id === id ? { ...field, value: event.target.value } : field
);
setFields(newFields);
};
return (
<form>
{fields.map(field => (
<div key={field.id}>
<label htmlFor={field.id}>Field:</label>
<input
id={field.id}
type="text"
value={field.value}
onChange={event => handleChange(field.id, event)}
/>
</div>
))}
<button type="button" onClick={addField}>Add Field</button>
</form>
);
}
export default DynamicForm;
4. SSR 改进(Server-Side Rendering Improvements)
React 18 对服务端渲染(SSR)进行了改进,支持流式渲染(Streaming Rendering),使得页面加载更快。
- 流式渲染:React 18 支持流式渲染,使得服务端可以在数据准备好后逐步发送 HTML 内容,从而提升首屏加载速度。
- Selective Hydration:允许在客户端根据需要逐步激活(hydrate)不同部分的 UI,从而提升性能。
新的 SSR API
React 18 引入了一些新的 SSR API,例如 renderToPipeableStream
和 renderToReadableStream
,用于支持流式渲染。React 18 引入了实验性的 React 服务器组件(React Server Components)
,允许在服务器端渲染组件并将其发送到客户端,从而减少客户端 JavaScript 的负担。
import { renderToPipeableStream } from 'react-dom/server';
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
pipe(response);
},
});
5. 新的 Strict Mode 行为
React 18 中的严格模式(Strict Mode)引入了更多的开发时候检查,帮助开发者发现潜在的问题。例如,严格模式下会模拟卸载和重新挂载组件,以确保组件在不同生命周期阶段的行为一致。
React 的 Strict Mode 主要用于在开发环境中帮助识别潜在问题,并不影响生产环境中的行为。React 18 对 Strict Mode 做了一些增强,尤其是在组件的挂载和卸载方面。以下是一些关键变化:
1. 双重渲染(Double Invoking)
在 React 18 的 Strict Mode 下,React 会在开发环境中对某些生命周期方法(如 componentDidMount
和 componentWillUnmount
)进行双重调用。这是为了帮助开发者发现副作用和潜在问题。具体来说,React 会执行以下步骤:
- 初次挂载:第一次渲染组件,并调用
componentDidMount
。 - 卸载:立即卸载组件,调用
componentWillUnmount
。 - 重新挂载:再次渲染组件,并再次调用
componentDidMount
。 这种行为的目的是确保组件在挂载和卸载过程中不会产生副作用。
2. 自动批处理(Automatic Batching)
React 18 引入了自动批处理功能,这意味着在事件处理程序之外的多个状态更新也会被自动批处理。在 Strict Mode 下,这种行为同样适用,有助于减少不必要的重新渲染。
3. 并发模式(Concurrent Mode)
虽然并发模式在 React 18 中并不是默认启用的,但它是 React 18 的一个重要特性。在 Strict Mode 下,React 会模拟并发渲染,帮助开发者识别和解决潜在的并发问题。
示例代码
以下是一个示例代码,展示了 React 18 中 Strict Mode 的新行为:
在这个示例中,useEffect
中的 console.log
语句会在组件挂载和更新时执行,而清理函数会在组件卸载时执行。在 React 18 的 Strict Mode 下,你会看到 console.log('Component mounted or updated')
被调用两次,这是因为组件在开发环境中会被双重渲染。
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom/client';
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Component mounted or updated');
return () => {
console.log('Component will unmount');
};
}, []);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
6. 新的 Suspense 功能
React 18 引入了新的 Suspense 功能,使得处理异步操作和数据加载变得更加方便和高效。通过这些新特性,开发者可以更好地管理异步数据加载和状态更新,从而提升用户体验。
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
1、Suspense for Data Fetching
假设我们有一个异步函数 fetchData
,用于获取数据:
const fetchData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Data fetched!");
}, 2000);
});
};
我们可以使用 Suspense 来等待数据加载:
import React, { Suspense, useState, useEffect } from 'react';
import ReactDOM from 'react-dom/client';
const DataComponent = React.lazy(() => fetchData().then(data => {
return { default: () => <div>{data}</div> };
}));
function App() {
return (
<div>
<h1>React 18 Suspense</h1>
<Suspense fallback={<div>Loading...</div>}>
<DataComponent />
</Suspense>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
在这个示例中,DataComponent
是一个懒加载组件,它会在数据加载完成后显示。在数据加载过程中,Suspense 会显示 fallback
内容(例如,"Loading...")。
2、SuspenseList
组件(实验性功能,线上使用有风险)
注意
SuspenseList
目前是实验性功能,需要启用实验性特性才能使用,可以通过安装特定的实验版本来启用这些特性npm install react@experimental react-dom@experimental
SuspenseList
只能用于协调Suspense
组件的显示顺序,不能用于其他类型的组件
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
const ComponentA = React.lazy(() => new Promise(resolve => {
setTimeout(() => resolve({ default: () => <div>Component A</div> }), 1000);
}));
const ComponentB = React.lazy(() => new Promise(resolve => {
setTimeout(() => resolve({ default: () => <div>Component B</div> }), 2000);
}));
function App() {
return (
<div>
<h1>React 18 SuspenseList</h1>
<SuspenseList revealOrder="together">
<Suspense fallback={<div>Loading A...</div>}>
<ComponentA />
</Suspense>
<Suspense fallback={<div>Loading B...</div>}>
<ComponentB />
</Suspense>
</SuspenseList>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
revealOrder
属性:控制显示顺序,可以是以下值:
forwards
:按顺序依次显示子组件。backwards
:按逆序依次显示子组件。together
:同时显示所有子组件。