了解React懒初始化(lazy initialization)及其原理

profile image

学习如何通过React useState的懒初始化(lazy initialization)优化性能,并通过React源码分析理解其工作原理。

本帖由 DeepL 翻译。如有任何翻译错误,请告知我们!

什么是懒初始化(lazy initialization)?

React中的懒初始化是一种在状态(state)初始化过程中,当初始化任务成本高或耗时长时非常有用的技术。

为什么需要懒初始化

让我们看看下面的代码示例。以下代码的问题是,每次React组件重新渲染时,传递给useState的初始值函数都会被调用。

typescript
const veryHeavyWork = () => {
  console.log('very heavy work called');

  // 访问localStorage、复杂计算、API调用等
  // 耗时的操作...

  return { data: 'expensive data' };
};

export const ProblemComponent = () => {
  // 问题:每次重新渲染时都会调用veryHeavyWork
  const [expensiveState, setExpensiveState] = useState(veryHeavyWork());
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        增加触发重新渲染
      </button>
    </div>
  );
};

在上面的代码中,每次点击"增加"按钮时都会调用veryHeavyWork函数。然而,由于expensiveState已经初始化并被react-reconciler记忆化,这个函数的返回值会被忽略,导致性能浪费。

console1.webp

应用懒初始化

在这种情况下,使用懒初始化可以防止在重新渲染期间再次执行昂贵的函数,如果它们已经被设置为初始值。方法很简单:不是传递函数执行的结果,而是传递一个返回函数的函数。

typescript
export const OptimizedComponent = () => {
  // 解决方案:传递一个返回函数的函数
  const [expensiveState, setExpensiveState] = useState(() => veryHeavyWork());
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        增加已优化
      </button>
    </div>
  );
};

console2.webp

换句话说,通过将执行时机委托给React而不是由开发者直接决定,可以解决这个问题。这种技术之所以被称为"懒初始化",是因为它延迟执行直到必要时才进行。

JavaScript的评估时机

初始化

在将参数传递给useState之前,veryHeavyWork函数在JavaScript代码评估阶段就被执行。由于重新渲染意味着重新执行组件函数,所以这个函数每次都会被调用。函数执行后,返回值被传递给useState,但这个值被忽略,因为react-reconciler使用存储在hook的memoizedState中的现有值。

typescript
const [expensiveState, setexpensiveState] = useState(veryHeavyWork());

重新渲染

然而,如果像下面这样编写,一个返回函数的函数在执行veryHeavyWork函数之前作为值传递useState。因此,它不会在JavaScript评估代码的时候执行。

typescript
const [expensiveState, setexpensiveState] = useState(() => veryHeavyWork());

React如何做出这个决定

初始化

React是如何做出这种判断的?让我们分析React代码

当提供初始值时,mountState被执行。我们可以看到它返回memoizedState和dispatch作为返回值。这些就是我们使用的const [state, setState] = useState()

typescript
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountStateImpl(initialState);
  // 省略...
  queue.dispatch = dispatch;
  return [hook.memoizedState, dispatch];
}

``

现在让我们看看实现,mountStateImpl。如果初始值作为函数传入,我们可以看到它将执行该函数的结果存储在initialState中,然后再将该值存储在memoizedState中。

typescript
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    const initialStateInitializer = initialState;
    initialState = initialStateInitializer();
    // 省略...
  }
  hook.memoizedState = hook.baseState = initialState;
  // 省略..
  return hook;
}

重新渲染

现在让我们看看重新渲染期间运行的代码。在重新渲染期间,rerenderStatererenderReducer按顺序运行。从代码中可以看出,它只是检索并返回先前存储的memoizedState

typescript
function rerenderReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  let newState = hook.memoizedState;

  // 如果在上一个渲染阶段调用了dispatch则执行
  if (lastRenderPhaseUpdate !== null) {
    // 省略
  }
  return [newState, dispatch];
}

什么时候应该使用它?

如上所述,当生成初始值需要很长时间计算时,使用它会很好。例如,当访问localStoragesessionStorage,或使用数组方法时。

  1. 访问localStorage/sessionStorage

    javascript
    const [user, setUser] = useState(() => {
      const savedUser = localStorage.getItem('user');
      return savedUser ? JSON.parse(savedUser) : null;
    });
  2. 生成复杂的初始数据

    javascript
    const [chartData, setChartData] = useState(() => {
      return generateComplexChartData(rawData);
    });
  3. 处理大型数组或对象

    javascript
    const [processedItems, setProcessedItems] = useState(() => {
      return largeDataSet.map(item => ({
        ...item,
        processed: true,
        timestamp: Date.now()
      }));
    });

与使用useEffect初始化的区别

useState的懒初始化与使用useEffect在组件渲染时初始化状态之间有几个区别。

初始化时机

  • 懒初始化: 在状态变量声明时直接设置值。它同步工作,并且在组件首次渲染之前设置值,因此可以在组件挂载和首次渲染发生之前使用该值。
  • useEffect: 由于它在组件渲染后初始化,你可能会注意到屏幕上的值变化。

使用场景

  • 懒初始化: 当你需要在组件挂载前使用值时
  • useEffect:对于异步任务或发生副作用时
❤️ 0
🔥 0
😎 0
⭐️ 0
🆒 0