一个新的架构设计必然是为了解决旧架构设计的痛点和问题,那我们就需要看旧的架构体系中存在哪些问题。
1. Class架构设计问题
1.1. 代码复用困难
在 Class 组件中,共享状态逻辑很麻烦,只能通过 高阶组件(HOC) 或 Render Props,代码变得复杂且难以维护。
Class 组件共享逻辑问题
scala
// 创建一个高阶组件(HOC)来共享逻辑
function withMousePosition(WrappedComponent) {
return class extends React.Component {
state = { x: 0, y: 0 };
componentDidMount() {
window.addEventListener("mousemove", this.handleMouseMove);
}
componentWillUnmount() {
window.removeEventListener("mousemove", this.handleMouseMove);
}
handleMouseMove = (event) => {
this.setState({ x: event.clientX, y: event.clientY });
};
render() {
return <WrappedComponent {...this.props} mouse={this.state} />;
}
};
}
// 使用高阶组件
class MyComponent extends React.Component {
render() {
return <h1>Mouse Position: {this.props.mouse.x}, {this.props.mouse.y}</h1>;
}
}
export default withMousePosition(MyComponent);
问题:
- 需要写一个 HOC,逻辑不直观
- 逻辑拆分困难,复用时组件层级变多
- 组件嵌套层级变深,导致 "Wrapper Hell"
1.2. Class 组件 this 指向困扰
Class 组件使用 this.setState
更新状态,导致 this
需要手动绑定,容易出错。
this 绑定问题
kotlin
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.increment = this.increment.bind(this); // 必须绑定
}
increment() {
this.setState({ count: this.state.count + 1 });
}
render() {
return <button onClick={this.increment}>Click Me: {this.state.count}</button>;
}
}
问题:
this
需要手动绑定,容易忘记,导致undefined
错误- 可以使用箭头函数
increment = () => {}
,但语法冗长
1.3. 生命周期方法不清晰
Class 组件的生命周期函数 componentDidMount
、componentDidUpdate
、componentWillUnmount
职责混杂,一个生命周期函数可能涉及多个逻辑,导致代码难以管理。
生命周期混乱
javascript
class Example extends React.Component {
componentDidMount() {
this.fetchData();
window.addEventListener("resize", this.handleResize);
}
componentWillUnmount() {
window.removeEventListener("resize", this.handleResize);
}
fetchData() {
// 获取数据
}
handleResize = () => {
// 处理窗口大小变化
};
render() {
return <div>Example Component</div>;
}
}
问题:
componentDidMount
里有多个逻辑(数据请求、监听事件)componentWillUnmount
需要手动清理副作用- 代码难以拆分和复用
2. Hooks 解决了这些问题
Hooks 让我们在 函数组件 里使用状态和生命周期逻辑,避免 this
问题,逻辑更清晰,代码更简洁。
2.1. useCustom Hook 解决状态存储和复用
自定义 Hook 让我们可以封装可复用的逻辑,不需要 HOC 也能复用状态。
javascript
import { useState, useEffect } from "react";
// 自定义 Hook
function useMousePosition() {
const [mouse, setMouse] = useState({ x: 0, y: 0 });
useEffect(() => {
const updateMouse = (e) => setMouse({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", updateMouse);
return () => {
window.removeEventListener("mousemove", updateMouse);
};
}, []);
return mouse;
}
// 组件中使用
function MyComponent() {
const mouse = useMousePosition();
return <h1>Mouse Position: {mouse.x}, {mouse.y}</h1>;
}
✔ 优势:
- 逻辑封装成
useMousePosition
,可复用 - 组件结构扁平,避免 "Wrapper Hell"
- 逻辑更清晰
2.2. useState 让函数组件有状态
javascript
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Click Me: {count}</button>;
}
✔ 优势:
- 去掉了 class,函数式更简单
- 不用手动绑定
this
,不会有this
相关 bug
2.3. useEffect 让函数组件有生命周期能力
javascript
import React, { useState, useEffect } from "react";
function Example() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize); // 清理副作用
};
}, []);
return <div>Window Width: {width}</div>;
}
✔ 优势:
useEffect
统一管理副作用,不再需要componentDidMount
和componentWillUnmount
- 避免了生命周期拆分困难的问题
3. Hooks是如何解决以上问题的呢?
3.1. Hooks 的状态存储和复用
- Class 组件状态存储
在 Class 组件 中, state
存在于 组件实例 上:
scala
class Counter extends React.Component {
constructor() {
super();
this.state = { count: 0 };
}
}
React 内部会创建 Counter
组件的 实例对象:
ini
const instance = new Counter();
-
- 状态存储在实例 (
instance.state
) 上 - this 让 state 成为组件的属性
- 复用逻辑必须靠继承、HOC 或者 Render Props
- 状态存储在实例 (
- Hooks 组件状态存储
Hooks 不使用类实例存储状态 ,而是存储在 React Fiber 的链表中:
scss
function Counter() {
const [count, setCount] = useState(0);
}
底层实现:
ini
let hookIndex = 0;
const hooks = [];
function useState(initialValue) {
if (!hooks[hookIndex]) {
hooks[hookIndex] = initialValue;
}
const setState = (newValue) => {
hooks[hookIndex] = newValue;
render(); // 重新渲染
};
return [hooks[hookIndex++], setState];
}
Hooks 组件是如何存储状态的?
-
- 每个函数组件都有一个
hooks
数组 ,存储useState
的状态 - 每次
useState
都会从hooks
数组里取值 - 组件重新渲染时,
useState
仍然能找到之前的值
- 每个函数组件都有一个
这样就不需要 Class 组件的 this.state
了。
3.2. 为什么 Hooks 没有 this
困扰?
- Hooks 是基于闭包的,而不是基于类实例
- 每次渲染时,Hooks 创建一个全新的作用域
useState
返回的是 局部变量,而不是实例属性 ,不需要this
底层实现分析
在 React 内部,Class 组件的 state
是绑定在实例上的:
scala
class Counter extends React.Component {
constructor() {
super();
this.state = { count: 0 };
}
}
而 Hooks 直接使用一个数组或链表来存储状态 ,避免 this
的问题:
scss
function Counter() {
const [count, setCount] = useState(0); // 这里 count 只是一个变量,不属于实例
}
在 函数组件重新执行时,它创建了一个新的作用域 ,count
变量 不会丢失 ,因为 React 通过一个 隐藏的数组存储每次渲染的状态。
3.3. 工作原理
Hooks 的运行机制 依赖 React Fiber 架构 ,它通过 链表结构管理每次组件的状态。
3.3.1. Fiber 是什么?
Fiber 是 React 内部维护的 组件更新数据结构 ,它类似于 链表结构:
kotlin
const fiber = {
type: Counter, // 组件类型
stateNode: null, // 组件实例
child: null, // 子 Fiber
sibling: null, // 兄弟 Fiber
return: null, // 父 Fiber
hooks: [], // 存储 useState/useEffect 的状态
};
Fiber 中的 hooks 数组存储组件的状态!
3.3.2. Hooks 在 Fiber 中的存储
ini
function useState(initialState) {
const oldHook = currentFiber?.hooks[hookIndex]; // 取上次渲染的状态
const hook = { state: oldHook ? oldHook.state : initialState };
function setState(newState) {
hook.state = newState;
render(); // 触发更新
}
currentFiber.hooks[hookIndex++] = hook; // 存入 Fiber
return [hook.state, setState];
}
存储过程:
- React 在 Fiber 结构中存储
useState
的状态 - 每次渲染时,Hooks 通过
hookIndex
取回上次的状态 setState
更新状态后,React 触发 重新渲染
4. Hooks 必须在最顶层调用?
- React 按顺序存储 Hooks
- 如果条件变化,Hook 调用顺序会错乱
- React 通过
hookIndex
读取状态,顺序变了就会报错