什么是懒初始化(lazy initialization)?
React中的懒初始化是一种在状态(state)初始化过程中,当初始化任务成本高或耗时长时非常有用的技术。
为什么需要懒初始化
让我们看看下面的代码示例。以下代码的问题是,每次React组件重新渲染时,传递给useState的初始值函数都会被调用。
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记忆化,这个函数的返回值会被忽略,导致性能浪费。
应用懒初始化
在这种情况下,使用懒初始化可以防止在重新渲染期间再次执行昂贵的函数,如果它们已经被设置为初始值。方法很简单:不是传递函数执行的结果,而是传递一个返回函数的函数。
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>
);
};
换句话说,通过将执行时机委托给React而不是由开发者直接决定,可以解决这个问题。这种技术之所以被称为"懒初始化",是因为它延迟执行直到必要时才进行。
JavaScript的评估时机
初始化
在将参数传递给useState
之前,veryHeavyWork
函数在JavaScript代码评估阶段就被执行。由于重新渲染意味着重新执行组件函数,所以这个函数每次都会被调用。函数执行后,返回值被传递给useState
,但这个值被忽略,因为react-reconciler使用存储在hook的memoizedState中的现有值。
const [expensiveState, setexpensiveState] = useState(veryHeavyWork());
重新渲染
然而,如果像下面这样编写,一个返回函数的函数在执行veryHeavyWork
函数之前作为值传递给useState
。因此,它不会在JavaScript评估代码的时候执行。
const [expensiveState, setexpensiveState] = useState(() => veryHeavyWork());
React如何做出这个决定
初始化
React是如何做出这种判断的?让我们分析React代码!
当提供初始值时,mountState
被执行。我们可以看到它返回memoizedState和dispatch作为返回值。这些就是我们使用的const [state, setState] = useState()
。
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountStateImpl(initialState);
// 省略...
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
``
现在让我们看看实现,mountStateImpl
。如果初始值作为函数传入,我们可以看到它将执行该函数的结果存储在initialState
中,然后再将该值存储在memoizedState
中。
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;
}
重新渲染
现在让我们看看重新渲染期间运行的代码。在重新渲染期间,rerenderState
→ rerenderReducer
按顺序运行。从代码中可以看出,它只是检索并返回先前存储的memoizedState
。
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];
}
什么时候应该使用它?
如上所述,当生成初始值需要很长时间计算时,使用它会很好。例如,当访问localStorage
或sessionStorage
,或使用数组方法时。
-
访问localStorage/sessionStorage
javascriptconst [user, setUser] = useState(() => { const savedUser = localStorage.getItem('user'); return savedUser ? JSON.parse(savedUser) : null; });
-
生成复杂的初始数据
javascriptconst [chartData, setChartData] = useState(() => { return generateComplexChartData(rawData); });
-
处理大型数组或对象
javascriptconst [processedItems, setProcessedItems] = useState(() => { return largeDataSet.map(item => ({ ...item, processed: true, timestamp: Date.now() })); });
与使用useEffect初始化的区别
useState
的懒初始化与使用useEffect
在组件渲染时初始化状态之间有几个区别。
初始化时机
- 懒初始化: 在状态变量声明时直接设置值。它同步工作,并且在组件首次渲染之前设置值,因此可以在组件挂载和首次渲染发生之前使用该值。
- useEffect: 由于它在组件渲染后初始化,你可能会注意到屏幕上的值变化。
使用场景
- 懒初始化: 当你需要在组件挂载前使用值时
- useEffect:对于异步任务或发生副作用时