在有需求时发现的新知识,总是特别难忘!
在 react 专案效能调整上,发现了最基础的玩意。
前言
因爲工作使用 React 开发,对于许久没碰的我来说,难免错过了许多好料。
这篇的原由是有一次「质疑」现有专案的写法:
const [list, setList] = useState(() => getList());
阅读他人的代码,先质疑,再理解。这也是一种成长的方式。
仔细查了,才发现原来这叫做「useState Lazy Initialization」。
回到最基础的文件上,可以找到这篇:Avoiding recreating the initial state 。
它就是在讲述使用useState
,传入function
可以使状态仅在初始时 render。
如果 function
需要花费较高的运算时间,甚至引起画面卡顿,那么这是一个有效提升效能的做法。
中文应该叫做:惰性初始 state。
这篇记录学习内容,阅读后你应该会知道 useState lazy initialization:
- 如何正确起手式。
- 概念与实践方式。
- 有无使用的差异。
一、初始化方式的两种模式
1. 直接传递初始值
const [todos, setTodos] = useState(createInitialTodos());
- 初始值会在 组件每次渲染时 计算。
- 如果
createInitialTodos()
运算较重,将会影响效能。
2. 传递函数进行 lazy initialization
const [todos, setTodos] = useState(() => createInitialTodos());
// or
const [todos, setTodos] = useState(createInitialTodos);
- React 只会在第一次渲染时执行该函数,并将结果作为初始状态。
- 避免在每次渲染时执行昂贵的计算。
3. 建立昂贵计算
尝试将createInitialTodos
弄得複杂一点,你会发现开始延迟了。
/**
* @param {number} depth
* @param {number} size
*/
function createInitialTodos(depth = 3, size = 100) {
if (depth === 0) {
return Array(size)
.fill(0)
.map((_, i) => i);
}
return Array(size)
.fill(0)
.map(() => createInitialTodos(depth - 1, size));
}
二、使用範例来比较效能
完整範例放在:https://github.com/explooosion/react-lazy-initialization
1. 初始 CRA 专案
使用一个乾净的专案来测试。
npx create-react-app my-app
[ App.css ]
记得先移除所有样式。
2. 建立两种 Components 比较
沿用刚刚昂贵计算的function createInitialTodos()
。
建立一个直接传递初始值的 ComponentAppWithDirectValue
。
[ App.js ]
function AppWithDirectValue() {
const [count, setCount] = useState(0);
console.time("Direct initializer");
const [todos, setTodos] = useState(createInitialTodos());
console.timeEnd("Direct initializer");
return (
<div>
<button onClick={() => setCount(count + 1)}>
AppWithDirectValue ({count})
</button>
</div>
);
}
- 在
useState
上下使用console.time
,console.timeEnd
记录时间。 - 在
useState
直接传入值createInitialTodos
。 - 使用一个
button
来触发re-render
。
以及建立一个传递函数进行 lazy initialization 的 Component AppWithLazyInitializer
。
[ App.js ]
function AppWithLazyInitializer() {
const [count, setCount] = useState(0);
console.time("Lazy initializer");
const [todos, setTodos] = useState(createInitialTodos);
console.timeEnd("Lazy initializer");
return (
<div>
<button onClick={() => setCount(count + 1)}>
AppWithLazyInitializer ({count})
</button>
</div>
);
}
- 同样在
useState
上下使用console.time
,console.timeEnd
记录时间。 - 在
useState
传入函数createInitialTodos
,你也可以传入() => createInitialTodos()
。 - 使用一个
button
来触发re-render
。
接着在 App
使用,就可以来比较啰!
[ App.js ]
export default function App() {
return (
<>
<AppWithDirectValue />
<AppWithLazyInitializer />
</>
);
}
完整代码:
[ App.js ]
// @ts-check
import React, { useState } from "react";
/**
* @param {number} depth
* @param {number} size
*/
function createInitialTodos(depth = 3, size = 100) {
if (depth === 0) {
return Array(size)
.fill(0)
.map((_, i) => i);
}
return Array(size)
.fill(0)
.map(() => createInitialTodos(depth - 1, size));
}
function AppWithDirectValue() {
const [count, setCount] = useState(0);
console.time("Direct initializer");
const [todos, setTodos] = useState(createInitialTodos());
console.timeEnd("Direct initializer");
return (
<div>
<button onClick={() => setCount(count + 1)}>
AppWithDirectValue ({count})
</button>
</div>
);
}
function AppWithLazyInitializer() {
const [count, setCount] = useState(0);
console.time("Lazy initializer");
const [todos, setTodos] = useState(createInitialTodos);
console.timeEnd("Lazy initializer");
return (
<div>
<button onClick={() => setCount(count + 1)}>
AppWithLazyInitializer ({count})
</button>
</div>
);
}
export default function App() {
return (
<>
<AppWithDirectValue />
<AppWithLazyInitializer />
</>
);
}
三、Lazy Initialization 合适的场景
✅ 适用于初始值计算量大,例如:
- 具有庞大的值或深度的资料。
- 複杂计算,例如来源是解析 JSON、深拷贝物件。
- 只需要在 第一次渲染 设定状态的情境。
❌ 不适用于:
- 简单的静态值,如
useState(0)
、useState(false)
。 - 不影响效能的初始化(过度使用 Lazy Initialization 反而让代码变得难读)。
reference
- react dev useState - avoiding-recreating-the-initial-state
- React Hooks 总整理笔记
- useState lazy initialization and function updates
有勘误之处,不吝指教。ob'_'ov