React 18 相对于 React 17 的主要升级内容
1. 并发特性(Concurrent Features)
React 18 引入了并发特性,使得 React 能够在后台准备多个版本的 UI,从而提升应用的响应速度和用户体验。
- 并发渲染(Concurrent Rendering) :允许 React 在后台准备多个版本的 UI,并在合适的时候应用这些更新。
useTransition
和startTransition
:这些 API 用于处理 UI 过渡状态,允许你将某些状态更新标记为低优先级,从而保持 UI 的响应性。useDeferredValue
:用于延迟更新某些状态,避免频繁的重新渲染,从而提升性能。
useTransition(react17)
useTransition
是 React 18 中新增的一个 Hook。它主要用于处理 UI 中的过渡状态,特别是在需要处理用户输入或者其他需要响应的操作时,可以让 React 在后台处理一些状态更新,从而避免 UI 的卡顿。 useTransition
返回一个布尔值和一个函数。布尔值表示当前是否处于过渡状态,函数用于启动过渡状态更新。
jsx
const [isPending, startTransition] = useTransition();
isPending
:一个布尔值,表示当前是否有过渡中的状态更新。startTransition
:一个函数,用于启动过渡状态更新。
下面是一个使用 useTransition
的示例,展示如何在处理大量数据时保持 UI 的响应性。
jsx
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 会在后台处理这些过渡状态更新,使得用户体验更加流畅。
2. 自动批处理(Automatic Batching)
React 18 引入了自动批处理更新的功能,即使是在异步事件中,多个状态更新也会被批处理在一起,从而减少不必要的重新渲染。
jsx
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 事件处理程序内的状态更新。也就是说,如果你在 React 事件处理程序中进行多次状态更新,React 会将这些状态更新自动批处理,以减少重新渲染的次数。这有助于提高性能。但是,在其他异步操作(如 setTimeout
、Promise
或者原生事件处理程序)中,React 17 并不会自动批处理状态更新。在 React 17 中,如果状态更新发生在异步操作中(如 setTimeout
或 Promise
),则不会自动批处理。
从 React 18 开始,自动批处理的范围被扩展到了所有的异步操作,包括 setTimeout
、Promise
、原生事件处理程序等。
例如以下代码,如果点击按钮,react17会log两次,而react18只会log一次
jsx
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一次
jsx
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;
3. useId
Hook
React 18 引入了 useId
Hook,通过使用 useId
,我们可以确保生成的 ID 是唯一且稳定的,避免 ID 冲突,并在服务器端渲染和客户端渲染之间保持一致性。
1、表单元素的关联
在表单中,我们通常需要为每个输入元素生成一个唯一的 ID,以便 label
标签能够正确地关联到相应的输入元素。使用 useId
可以简化这个过程,并确保生成的 ID 是唯一且稳定的
jsx
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。
jsx
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,从而提升性能。
5. 新的 SSR API
React 18 引入了一些新的 SSR API,例如 renderToPipeableStream
和 renderToReadableStream
,用于支持流式渲染。React 18 引入了实验性的 React 服务器组件(React Server Components)
,允许在服务器端渲染组件并将其发送到客户端,从而减少客户端 JavaScript 的负担。
javascript
import { renderToPipeableStream } from 'react-dom/server';
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
pipe(response);
},
});
6. 新的 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 的新行为:
jsx
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>
);
在这个示例中,useEffect
中的 console.log
语句会在组件挂载和更新时执行,而清理函数会在组件卸载时执行。在 React 18 的 Strict Mode 下,你会看到 console.log('Component mounted or updated')
被调用两次,这是因为组件在开发环境中会被双重渲染。
7. 新的 Suspense 功能
React 18 引入了新的 Suspense 功能,使得处理异步操作和数据加载变得更加方便和高效。通过这些新特性,开发者可以更好地管理异步数据加载和状态更新,从而提升用户体验。
jsx
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
,用于获取数据:
jsx
const fetchData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Data fetched!");
}, 2000);
});
};
我们可以使用 Suspense 来等待数据加载:
jsx
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
组件可以协调多个 Suspense 组件的显示顺序:
jsx
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 />);
在这个示例中,SuspenseList
使用 revealOrder="together"
,表示所有 Suspense 子组件会一起显示