React中的Hook到底是个什么鬼
文章目录
- Hooks本质:
- Hooks的核心特点:
- Hooks的设计原理
- Hooks 的底层实现(简化版)
- Hooks 的最佳实践
- Hooks 与 Class 的对比
- 为什么Hooks依赖调用顺序来正确关联状态与组件实例
- 如何理解Hooks中的闭包机制
React Hooks 是 React 16.8 引入的函数组件增强机制,它允许你在不编写 class 的情况下使用 state 和其他 React 特性。
简单来说,Hooks 是 React 提供的一系列特殊函数,它们能让你"钩入"(hook into)React 的核心功能。
Hooks本质:
- 函数组件的扩展工具:让函数式组件可以拥有类组件功能(状态、生命周期等)
- 逻辑复用功能:代替高阶组件和render props的复杂模式
- 代码组织方式:按照功能而非生命周期组织代码
Hooks的核心特点:
1、状态管理(useState)
function Counter() {const [count, setCount] = useState(0); // 状态声明return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
函数式组件可以拥有自己的状态,React在内部维护了一个“记忆单元”链表,保存Hook的状态
2、副作用管理(useEffect)
useEffect(() => {document.title = `You clicked ${count} times`;return () => { /* 清理逻辑 */ }; // 类似componentWillUnmount
}, [count]); // 依赖数组
代替了componentDidMount + componentDidUpdate + componentWillUnmount。可以将相关逻辑集中在一起,而非分散在不同生命周期上
3、上下文访问
const value = useContext(MyContext); // 直接获取context值
无需再使用Consumer组价包裹
Hooks的设计原理
1、调用顺序稳定性
- react依赖Hooks的调用顺序来正确关联状态
- 因此Hooks不能放在条件判断、循环语句中
2、闭包机制
function Counter() {const [count, setCount] = useState(0);useEffect(() => {const id = setInterval(() => {console.log(count); // 永远捕获声明时的count值}, 1000);return () => clearInterval(id);}, []);return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
- 每次渲染都有独立的 props/state(闭包特性)
- 通过函数式更新解决闭包问题:setCount(c => c + 1)
3、调度机制
- useState/setState 会触发重新渲染
- useEffect 在浏览器完成布局与绘制后异步执
Hooks 的底层实现(简化版)
let hooks = [];
let currentHook = 0;function useState(initialValue) {const _currentHook = currentHook++;if (hooks[_currentHook] === undefined) {hooks[_currentHook] = initialValue;}const setState = (newValue) => {hooks[_currentHook] = newValue;render(); // 触发重新渲染};return [hooks[_currentHook], setState];
}function useEffect(callback, deps) {const _currentHook = currentHook++;const hasChanged = !deps || !hooks[_currentHook] || deps.some((d, i) => d !== hooks[_currentHook][i]);if (hasChanged) {callback();hooks[_currentHook] = deps;}
}
Hooks 的最佳实践
1、命名规范:自定义Hook必须以“use”开头
2、条件调用:只有在最顶层调用Hooks
3、性能优化
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
4、自定义Hooks:提取可复用逻辑
function useWindowSize() {const [size, setSize] = useState({ width: window.innerWidth });useEffect(() => {const handler = () => setSize({ width: window.innerWidth });window.addEventListener('resize', handler);return () => window.removeEventListener('resize', handler);}, []);return size;
}
Hooks 与 Class 的对比
特性 | Hooks | Class Components |
---|---|---|
状态管理 | useState/useReducer | this.state |
生命周期 | useEffect | componentDidMount 等 |
代码组织 | 按功能组织 | 按生命周期拆分 |
逻辑复用 | 自定义 Hooks | HOC/render props |
this绑定 | 无this绑定问题 | 需要处理this |
学习曲线 | 较简单 | 较复杂 |
为什么Hooks依赖调用顺序来正确关联状态与组件实例
React Hooks 依赖调用顺序稳定性来正确关联状态与组件实例,这是其核心设计原理之一。这种限制源于 Hooks 的实现机制
一、底层实现机制
React在内部使用链表结构管理Hooks状态,每次渲染时:
1、按顺序记录Hooks调用(如useState->useEffect->useState)
2、用链表节点保存对应状态(节点位置与调用顺序严格对应)
3、下次渲染时候按相同顺序读取
就类似于一个军训的点名场景:
教官按照固定顺序进行点名(调用顺序)
每位学生(Hook)必须每次都站在队伍中相同的位置
教官通过位置编号而非名字来识别每一位学生
具体工作流程分析:
1、react内部会维护一个“记忆链表”,结构如下:
[ useState → useEffect → useMemo → useState → … ]
↓ ↓ ↓ ↓
state effect memoized state
2、渲染更新过程
组件首次渲染:
function Example() {const [count, setCount] = useState(0); // Hook 1const [name, setName] = useState('Alice'); // Hook 2useEffect(() => { /*...*/ }); // Hook 3
}
内部链表:
- useState(0) → 节点A { value: 0 }
- useState(‘Alice’) → 节点B { value: ‘Alice’ }
- useEffect → 节点C { effectFn, deps }
组件更新渲染:
function Example() {const [count, setCount] = useState(0); // 读取节点Aconst [name, setName] = useState('Alice'); // 读取节点BuseEffect(() => { /*...*/ }); // 读取节点C
}
工作流程:
1、按顺序遍历链表
2、第一个 useState 读取节点A的状态
3、第二个 useState 读取节点B的状态
4、useEffect 读取节点C的信息
二、条件语句为何破坏稳定性
function MyComponent({ showExtra }) {if (showExtra) {const [extra, setExtra] = useState(null); // 🔴 条件性 Hook}const [count, setCount] = useState(0); // ⚠️ 调用顺序可能变化
}
渲染过程:
当showExtra条件不变时候,第一次渲染和第二次渲染顺序就是一致的,当条件变化时候,顺序就会发生变化,如第一次为true,那么记录的位置是useState(null)->useState(0),当第二次为false时候,useState(0),那么第二次的位置就会读取到读取第一次的状态,这样值就是错乱的
三、同样的循环语句也会破坏稳定性
function DynamicHooks({ items }) {return items.map((item, i) => {const [value, setValue] = useState(item); // 🔴 循环中的 Hookreturn <div key={i}>{value}</div>;});
}
1、Hooks调用次数变化:items.length改变会导致Hooks数量不一致
2、状态关联错乱:新增、删除等会使Hooks的读取错误位置的状态
四、React的Hooks必须在函数组件的最顶层且无条件的调用
1、状态关联错乱:Hooks与组件的状态是通过调用顺序关联的,条件调用会导致Hooks在不同条件渲染下对应到错误的状态
2、调试困难:难以追踪那些Hooks被调用、被跳过;导致难以复现的bug
3、性能优化失效:React依赖稳定的Hooks顺序进行优化,动态的Hooks会破坏memoization 和 bailout 机制
五、如何解决
1、编译时检查:通过ESLint 插件强制规则
// eslint-disable-react-hooks/rules-of-hooks
2、运行时报错:开发模式下React会抛出错误
六、如何正确实现条件、循环逻辑
方式一:提前调用所有的Hook
function MyComponent({ showExtra }) {const [extra, setExtra] = useState(null); // 始终调用const [count, setCount] = useState(0);return (<>{showExtra && <div>{extra}</div>}<div>{count}</div></>);
}
方式二:拆分组件
function ExtraComponent() {const [extra, setExtra] = useState(null); // 在独立组件中使用return <div>{extra}</div>;
}function MyComponent({ showExtra }) {const [count, setCount] = useState(0);return (<>{showExtra && <ExtraComponent />}<div>{count}</div></>);
}
如何理解Hooks中的闭包机制
React Hooks 的闭包机制是其核心设计特性之一,理解这一机制对于正确使用 Hooks 至关重要,下面详解:
一、闭包机制的本质
在 JavaScript 中,闭包是指函数能够访问并记住其词法作用域的特性。在 React 函数组件中:
1、每次渲染都是一个独立的函数调用
2、每次调用都会创建新的作用域和闭包
3、所有 props 和 state 都被"捕获"在该次渲染的闭包中
function Counter() {const [count, setCount] = useState(0);useEffect(() => {const id = setInterval(() => {console.log(count); // 永远捕获声明时的count值}, 1000);return () => clearInterval(id);}, []);return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
渲染过程中闭包的表现:
1、首次渲染(count=0)
// 闭包1
const count = 0;
useEffect(() => {// 这里的count永远为0setInterval(() => console.log(0), 1000);
}, []);
2、点击按钮后(count=1)
// 闭包2
const count = 1;
// 但之前useEffect中的回调仍然引用闭包1的count(0)
3、事件函数中的闭包
function Example() {const [count, setCount] = useState(0);function handleClick() {setTimeout(() => {console.log(count); // 点击时的count值}, 3000);}return <button onClick={handleClick}>Click</button>;
}
- 每次点击都会创建一个新的handleClick函数
- 每个函数都捕获了当次渲染时的count值
解决闭包问题的方案:
1、函数式更新:
setCount(c => c + 1); // 获取最新状态
2、useRef 保存可变值
const countRef = useRef(count);
useEffect(() => {countRef.current = count; // 每次渲染后更新
});// 在任何闭包中访问 countRef.current
3、使用 useReducer
const [state, dispatch] = useReducer(reducer, { count: 0 });
// dispatch总是引用同一个函数
如何理解所有 props 和 state 都被"捕获"在该次渲染的闭包中
在上述代码中我们已经理解了闭包机制的本质。在React组件中,每次渲染都是一个独立的函数调用过程,这个过程会创建一个新的闭包作用域,在这个作用域中:
- 所有的props和state值都会被“冻结”,就想拍照一样,记录当下渲染时的值
- 所有函数定义都会被绑到这个闭包中,包括事件函数、effect回调等
- 即便外部组件重新渲染,这个闭包中的值也不会被改变,即保持渲染时的快照
举例:
function Counter({ initialCount }) {const [count, setCount] = useState(initialCount);function handleClick() {setTimeout(() => {console.log(`Count at render: ${count}, Prop: ${initialCount}`);}, 3000);}return (<div><button onClick={handleClick}>Show Values</button><button onClick={() => setCount(c => c + 1)}>Increment</button></div>);
}
上述代码中假设initialCount 初始值是0
首次渲染时候,initialCount 为0 count也是0
当点击按钮后,count的值变为1 initialCount 依然是0
如果在第一次渲染后点击 Show Values,3秒后将打印
Count at render: 0, Prop: 0
即便组件已经更新 重新渲染了 但是旧的值依然是旧的值 因为在旧的闭包中仍然保持着它创建时的值