跳到主要内容

react 异步渲染如何理解

异步渲染(Async Rendering)是指 React 在更新组件时,能够以异步的方式执行渲染操作,从而提高性能和用户体验。在异步渲染中,React 会将渲染工作分割成多个小任务,这些小任务可以在多帧中逐渐完成,避免了长时间的主线程阻塞,使得页面更加流畅响应。

在 React 中,异步渲染是由 React 的调度器(Scheduler)来管理的。React 16.8 版本引入了 useStateuseEffect 等 Hook,以及 React 17 版本引入的并发模式(Concurrent Mode)都是基于异步渲染的概念。

以下是一个简单的示例,演示了 React 的异步渲染特性。由于 React 使用异步渲染,即使计算操作很耗时,也不会阻塞主线程。

import React, { useState, useEffect } from 'react';

function AsyncRenderingExample() {
const [count, setCount] = useState(0);
const [result, setResult] = useState(null);

useEffect(() => {
// 模拟一个耗时的计算操作
const calculateResult = () => {
let sum = 0;
for (let i = 1; i <= count; i++) {
sum += i;
}
return sum;
};

// 使用定时器模拟异步操作
const timer = setTimeout(() => {
const calculatedResult = calculateResult();
setResult(calculatedResult);
}, 1000);

// 清除定时器以防止内存泄漏
return () => clearTimeout(timer);
}, [count]);

return (
<div>
<h1>Async Rendering Example</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
{result !== null && <p>Result of the calculation: {result}</p>}
</div>
);
}

export default AsyncRenderingExample;

问题 1:以上代码,useEffect 最后清除定时器,定时器的内容为什么还会执行?

在 React 中,useEffect 是一个副作用函数,它可以执行异步操作、订阅数据、手动修改 DOM 等。当使用 useEffect 时,返回的函数(清除函数)会在组件销毁时执行,用来清理副作用。在你的代码中,useEffect 返回的清除函数清除了定时器。

你的疑问可能出现在定时器执行的时机。在 React 中,由于函数组件在每次渲染时都会重新执行,useEffect 中的函数也会重新执行。在每次组件重新渲染时,新的定时器会被创建,旧的定时器会被清除。所以,每次点击 "Increment" 按钮,都会清除上一次的定时器,然后创建一个新的定时器。

这样的行为导致了在计算结果返回之前,可能会出现多个定时器同时存在的情况。当定时器的回调函数执行时,它会引用当前渲染周期内的 count 变量。这就是为什么你会看到,即使定时器被清除,它仍然可以引用到之前的 count 值。

为了确保在每个渲染周期只有一个定时器存在,你可以使用 useRef 来存储定时器的引用,并在 useEffect 中清除旧的定时器。这样可以确保在下一次渲染周期时,只有一个定时器在执行。

以下是修改后的代码示例:

import React, { useState, useEffect, useRef } from 'react';

function AsyncRenderingExample() {
const [count, setCount] = useState(0);
const [result, setResult] = useState(null);
const timerRef = useRef(null);

useEffect(() => {
// 清除旧的定时器
if (timerRef.current) {
clearTimeout(timerRef.current);
}

// 模拟一个耗时的计算操作
const calculateResult = () => {
let sum = 0;
for (let i = 1; i <= count; i++) {
sum += i;
}
return sum;
};

// 使用定时器模拟异步操作
timerRef.current = setTimeout(() => {
const calculatedResult = calculateResult();
setResult(calculatedResult);
}, 1000);

// 清除定时器以防止内存泄漏
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [count]);

return (
<div>
<h1>Async Rendering Example</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
{result !== null && <p>Result of the calculation: {result}</p>}
</div>
);
}

export default AsyncRenderingExample;

在这个修改后的代码中,使用 useRef 创建了一个 timerRef,用来存储定时器的引用。每次 useEffect 执行时,它会清除旧的定时器,并将新的定时器引用保存在 timerRef.current 中。这样可以确保在每个渲染周期内只有一个定时器存在。

问题 2:当 useEffect 执行,组件重新渲染,为什么 useState 的值不会重置为初始值?

在 React 函数组件中,每次组件重新渲染时,useState 返回的状态值不会重置为初始值。这是因为 React 通过使用闭包来保留useState返回的当前状态值。当组件重新渲染时,React 会记住当前的闭包,因此状态值在不同渲染周期之间得以保持。