Skip to content

1. 对 React Hook 的理解,它的实现原理是什么

React-Hooks 是 React 团队在 React 组件开发实践中,逐渐认知到的一个改进点,这背后其实涉及对类组件函数组件两种组件形式的思考和侧重。

(1)类组件:

所谓类组件,就是基于 ES6 Class 这种写法,通过继承 React.Component 得来的 React 组件。以下是一个类组件:

js
class DemoClass extends React.Component {
  state = {
    text: ""
  };
  componentDidMount() {
    //...
  }
  changeText = (newText) => {
    this.setState({
      text: newText
    });
  };

  render() {
    return (
      <div className="demoClass">
        <p>{this.state.text}</p>
        <button onClick={this.changeText}>修改</button>
      </div>
    );
  }
}

可以看出,React 类组件内部预置了相当多的“现成的东西”等着我们去调度/定制,state 和生命周期就是这些“现成东西”中的典型。要想得到这些东西,难度也不大,只需要继承一个 React.Component 即可。

当然,这也是类组件的一个不便,它太繁杂了,对于解决许多问题来说,编写一个类组件实在是一个过于复杂的姿势。复杂的姿势必然带来高昂的理解成本,这也是我们所不想看到的。除此之外,由于开发者编写的逻辑在封装后是和组件粘在一起的,这就使得类组件内部的逻辑难以实现拆分和复用。

(2)函数组件:

函数组件就是以函数的形态存在的 React 组件。早期并没有 React-Hooks,函数组件内部无法定义和维护 state,因此它还有一个别名叫“无状态组件”。以下是一个函数组件:

js
function DemoFunction(props) {
  const { text } = props
  return (
    <div className="demoFunction">
      <p>{`函数组件接收的内容:[${text}]`}</p>
    </div>
  );
}

相比于类组件,函数组件肉眼可见的特质自然包括轻量、灵活、易于组织和维护、较低的学习成本等。

通过对比,从形态上可以对两种组件做区分,它们之间的区别如下:

  • 类组件需要继承 class,函数组件不需要;
  • 类组件可以访问生命周期方法,函数组件不能;
  • 类组件中可以获取到实例化后的 this,并基于这个 this 做各种各样的事情,而函数组件不可以;
  • 类组件中可以定义并维护 state(状态),而函数组件不可以;

除此之外,还有一些其他的不同。通过上面的区别,我们不能说谁好谁坏,它们各有自己的优势。在 React-Hooks 出现之前,类组件的能力边界明显强于函数组件。

实际上,类组件和函数组件之间,是面向对象和函数式编程这两套不同的设计思想之间的差异。而函数组件更加契合 React 框架的设计理念:

image.png

React 组件本身的定位就是函数,一个输入数据、输出 UI 的函数。作为开发者,我们编写的是声明式的代码,而 React 框架的主要工作,就是及时地把声明式的代码转换为命令式的 DOM 操作,把数据层面的描述映射到用户可见的 UI 变化中去。这就意味着从原则上来讲,React 的数据应该总是紧紧地和渲染绑定在一起的,而类组件做不到这一点。函数组件就真正地将数据和渲染绑定到了一起。函数组件是一个更加匹配其设计理念、也更有利于逻辑拆分与重用的组件表达形式。

为了能让开发者更好的的去编写函数式组件。于是,React-Hooks 便应运而生。

React-Hooks 是一套能够使函数组件更强大、更灵活的“钩子”。

函数组件比起类组件少了很多东西,比如生命周期、对 state 的管理等。这就给函数组件的使用带来了非常多的局限性,导致我们并不能使用函数这种形式,写出一个真正的全功能的组件。而React-Hooks 的出现,就是为了帮助函数组件补齐这些(相对于类组件来说)缺失的能力。

如果说函数组件是一台轻巧的快艇,那么 React-Hooks 就是一个内容丰富的零部件箱。“重装战舰”所预置的那些设备,这个箱子里基本全都有,同时它还不强制你全都要,而是允许你自由地选择和使用你需要的那些能力,然后将这些能力以 Hook(钩子)的形式“钩”进你的组件里,从而定制出一个最适合你的“专属战舰”。

2.React Hook的实现原理

1. 基于 Fiber 的内部数据结构

  • Fiber 节点的 memoizedState
    每个组件在内部都有一个 Fiber 节点,用于存储组件的状态。React 将所有 Hook 的状态按照调用顺序保存到 Fiber 节点的一个链表中(通常称为 memoizedState)。
  • 顺序调用的重要性
    Hooks 的调用顺序必须保持一致,这样 React 才能正确地在链表中找到对应的 Hook 状态。这也是为什么**不能在条件判断或循环中调用 Hooks **的原因。

2.Fiber(纤程)是什么

Fiber,本意为 “纤维”,在计算机世界中则是 ”纤程“ 的意思。纤程可以看作是协程的一种,是一种任务调度方式。

JavaScript 是单线程的,有一个 event loop 的概念,它有一个有优先级的任务队列,只能按顺序执行一个任务,是不支持多个任务同时执行的。

这种设计的好处就是不用考虑多线程导致的顺序问题,并为此做一些加锁的额外逻辑,确保执行顺序符合预期。但也因为无法使用并行能力,在 CPU 密集的场景会有性能问题, 比如一个任务耗时过长会导致其他的任务,导致用户的交互响应发生延迟。

​React 的组件更新是 CPU 密集的操作,因为它要做对比新旧虚拟 DOM 树的操作(diff,React 中 Reconcilation 负责),找出需要更新的内容(patch),通过打补丁的方式更新真实 DOM 树(React 中 Renderer 负责)。当要对比的组件树非常多时,就会发生大量的新旧节点对比,CPU 花费时间庞大,当耗时大大超过 16.6ms(一秒 60 帧的基准) 时,用户会感觉到明显的卡顿。

这一系列操作是通过递归的方式实现的,是 同步且不可中断 的。因为一旦中断,调用栈就会被销毁,中间的状态就丢失了。这种基于调用栈的实现,我们称为 Stack Reconcilation。

React 16 的一个重点工作就是优化更新组件时大量的 CPU 计算,最后选择了使用 “时间分片” 的方案,就是将原本要一次性做的工作,拆分成一个个异步任务,在浏览器空闲的时间时执行。这种新的架构称为 Fiber Reconcilation。

在 React 中,Fiber 模拟之前的递归调用,具体通过链表的方式去模拟函数的调用栈,这样就可以做到中断调用,将一个大的更新任务,拆分成小的任务,并设置优先级,在浏览器空闲的时异步执行。

FiberNode

前面我们说到使用了链表的遍历来模拟递归栈调用,其中链表的节点 React 用 FiberNode 表示。

FiberNode 其实就是虚拟 DOM,它记录了:

  1. 节点相关类型,比如 tag 表示组件类型、type 表示元素类型等。
  2. 节点的指向。
  3. 副作用相关的属性。
  4. lanes 是关于调度优先级的
js
function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag; // 组件类型,比如 Function/Class/Host
  this.key = key; // key 唯一值,通常会在列表中使用
  this.elementType = null;
  this.type = null; // 元素类型,字符串或类或函数,比如 "div"/ComponentFn/Class
  this.stateNode = null; // 指向真实 DOM 对象
  
  // Fiber
  this.return = null; // 父 Fiber
  this.child = null; // 子 Fiber 的第一个
  this.sibling = null; // 下一个兄弟节点
  this.index = 0; // 在同级兄弟节点中的位置
  this.ref = null;
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;
  this.mode = mode;
  
  // Effects
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
  this.alternate = null; 
  // ...
}

Fiber 通过 return 指向父 Fiber,child 指向子 Fiber 的首位、sibling 指向下一个兄弟节点。通过它们我们其实就能拿到一个完整的结构树。

对于:

js
function App() {
  return (
    <div className="app">
      <span>hello</span>, Fiber
    </div>
  );
}

形成的 Fiber 树为:

其中弧线为调用顺序。紫色为 beginWork、粉色为 completeWork。beginWork 是 “递” 的过程,而 comleteWork 则是 “归” 的过程。

为什么不用 generator 或 async/await?

generator 和 async/await 也可以做到在函数中间暂停函数执行的逻辑,将执行让出去,能做到将同步变成异步。

但 React 没有选择它们,这是因为:

  1. 具有传染性,比如一个函数用了 async,调用它的函数就要加上 async,有语法开销,此外也会有性能上的额外开销。
  2. 无法在 generator 和 async/await 中恢复一些中间状态。

具体见官方的 github issue 讨论:

做了时间分片,拆分了多个任务,React 就可以以此为基石,给任务设置优先级。

React 实现了一个 Scheduler(调度器)来实现任务调度执行,并单独抽离为一个单独的包,它会在浏览器有空闲的时候执行。其实浏览器也提供了一个 requestIdleCallback 的 API,支持这个能力,但兼容性实在不好,React 还是自己实现了一套。

这个 Scheduler 支持优先级,底层使用了 小顶堆,确保能高效拿到最快要过期的任务,然后执行它。

> 小顶堆,其实就是优先级队列。小顶堆在结构上是一个完全二叉树,但能保证每次从堆顶取出元素时,是最小的元素。

任务的 优先级 分为几种:

  1. NoPriority:无优先级。
  2. ImmediatePriority:立即执行。
  3. UserBlockingPriority:用户阻塞优先级,不执行可能会导致用户交互阻塞。
  4. NormalPriority:普通优先级。
  5. LowPriority:低优先级。
  6. IdlePriority:空闲优先级。

React 自身也有优先级,叫做 Lane,两者是不同的。

2. Hooks API 的工作机制

  • useState
    当调用 useState 时,React 会检查当前 Fiber 节点对应的 hook 链表。如果是初次渲染,会创建一个新的 hook 状态节点;如果是更新渲染,会取出之前存储的状态。返回值包括当前状态和更新状态的函数,调用更新函数会触发组件重新渲染,并在新的渲染中沿用同一位置的 hook 节点。

  • useEffect 和其他副作用 Hooks
    当调用 useEffect(或 useLayoutEffect)时,React 将副作用函数以及其依赖项存储在对应的 hook 节点中。渲染完成后,React 会在 commit 阶段执行这些副作用,并根据依赖项决定是否重新执行。


3. Hooks 调度与更新

  • 调度器
    React 在调度更新时,会遍历当前组件的 hook 链表,从头开始依次调用每个 hook 的函数。这保证了每个 hook 调用对应到正确的状态存储位置。

  • 闭包和更新队列
    对于 useState,React 内部会为每个状态 hook 维护一个更新队列。调用状态更新函数时,新的状态更新会被加入到这个队列中,随后在下一次渲染中,这个队列会被处理,生成最新的状态值。


4. 实现原理总结

  • 数据存储
    每个函数组件的所有 Hooks 状态都存储在其对应 Fiber 的一个链表中,按照调用顺序排列。

  • 顺序和稳定性
    Hooks 的调用顺序必须一致,这样才能在多次渲染过程中保持状态正确匹配(即“Hook 链表”中的位置不变)。

  • 调度更新
    当状态更新函数被调用时,React 将更新信息存入更新队列,并在下一个调度周期中重新渲染组件,沿用相同的 hook 调用顺序来恢复和更新状态。


总结

React Hooks 的实现依赖于 Fiber 架构中的一个链表结构,将每个 hook 的状态和副作用信息按照调用顺序存储在 Fiber 节点上。Hooks API(如 useState、useEffect 等)通过访问这个链表,实现了状态管理和副作用处理的能力,同时保证了函数组件的简洁性和逻辑复用性。这种设计既利用了函数式编程的优势,也充分发挥了 React 内部调度与更新的机制,从而使得函数组件能够高效地管理状态和副作用。

3.常用的React Hook有哪些

React Hook 让你在 函数组件 中使用状态和生命周期特性,无需使用 class。常用 Hook 分为:

  • 基础 HookuseStateuseEffectuseRef 等)
  • 性能优化 HookuseMemouseCallback
  • 上下文管理 HookuseContext
  • 自定义 Hook(组合多个 Hook)

1. 基础 Hook

1.1 useState(管理状态)

用于定义组件内部的状态,每次状态更新,组件都会重新渲染。

javascript
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>当前计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}

📌 特点

  • 组件渲染时 不会丢失状态
  • setState 是异步的,批量更新。

1.2 useEffect(副作用管理)

用于处理副作用(数据请求、事件监听、修改 DOM)。

javascript
import { useState, useEffect } from "react";

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `计数: ${count}`; // 副作用:修改标题
  }, [count]); // 仅当 count 变化时执行

  return <button onClick={() => setCount(count + 1)}>增加</button>;
}

📌 执行时机

  • 默认:组件渲染后(非阻塞渲染)

  • 依赖更新时[dependency]

  • 组件卸载时执行 cleanup

    javascript
    useEffect(() => {
      const handleScroll = () => console.log("滚动中...");
      window.addEventListener("scroll", handleScroll);
      
      return () => window.removeEventListener("scroll", handleScroll);
    }, []);

1.3 useRef(获取 DOM 元素 / 保存变量)

  • 获取 DOM 元素(类似 document.getElementById)。
  • 保存跨渲染变量(不会触发重新渲染)。
javascript
import { useRef, useEffect } from "react";

function InputFocus() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus(); // 组件渲染后自动聚焦
  }, []);

  return <input ref={inputRef} type="text" />;
}

📌 特点

  • useRef 不会导致组件重新渲染
  • 可以保存跨渲染的值(类似 this)。

2. 性能优化 Hook

2.1 useMemo(缓存计算结果)

用于避免不必要的计算,仅当依赖变化时才重新计算。

javascript
import { useState, useMemo } from "react";

function ExpensiveCalculation({ num }) {
  const result = useMemo(() => {
    console.log("计算中...");
    return num ** 2; // 计算平方
  }, [num]); // 仅当 num 变化时重新计算

  return <p>结果: {result}</p>;
}

📌 适用场景

  • 避免重复计算
  • 优化大数组计算、过滤等。

2.2 useCallback(缓存函数)

用于避免不必要的函数重新创建,防止子组件重复渲染。

javascript
import { useState, useCallback } from "react";

function Parent() {
  const [count, setCount] = useState(0);

  // 仅在 count 变化时重新创建函数
  const handleClick = useCallback(() => {
    console.log("点击了");
  }, [count]);

  return <Child onClick={handleClick} />;
}

function Child({ onClick }) {
  return <button onClick={onClick}>点击</button>;
}

📌 适用场景

  • 子组件优化(React.memo
  • 避免函数对象变化导致不必要的 re-render

3. 上下文管理 Hook

3.1 useContext(跨组件状态管理)

用于在组件树中共享状态,代替 prop drilling(多层级传递)。

javascript
import { useContext, createContext } from "react";

const ThemeContext = createContext("light");

function ThemeButton() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>主题按钮</button>;
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemeButton />
    </ThemeContext.Provider>
  );
}

📌 适用场景

  • 全局状态管理(如 主题用户信息)。
  • 适合中小项目,大型项目建议用 Redux/Zustand

4. 其他 Hook

4.1 useReducer(复杂状态管理)

  • 适用于复杂逻辑(类似 Redux)。
javascript
import { useReducer } from "react";

const reducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      return state;
  }
};

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
}

📌 适用场景

  • 适合多状态复杂逻辑,比 useState 结构清晰。

4.2 useImperativeHandle(自定义 ref

  • 控制 ref 公开方法,防止外部直接操作。
javascript
import { useRef, useImperativeHandle, forwardRef } from "react";

const Input = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({
    focus: () => alert("手动触发 focus"),
  }));

  return <input type="text" />;
});

function App() {
  const inputRef = useRef();

  return (
    <>
      <Input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>点击触发</button>
    </>
  );
}

📌 适用场景

  • 自定义 ref,如手动触发弹窗、动画

5. 总结

Hook作用
useState组件状态管理
useEffect处理副作用(请求、DOM 操作)
useRef获取 DOM,保存变量(不触发渲染)
useMemo缓存计算结果
useCallback缓存函数,防止子组件重复渲染
useContext跨组件共享状态
useReducer复杂状态管理(类似 Redux)
useImperativeHandle自定义 ref 公开方法

🚀 结论

  • 状态管理:useState / useReducer
  • 副作用:useEffect
  • 优化性能:useMemo / useCallback
  • 获取 DOM:useRef
  • 全局状态管理:useContext

💡 合理搭配 Hook,提升 React 组件性能和可读性! 🚀

4. 为什么 useState 要使用数组而不是对象

useState 返回的是一个数组,这个设计主要有以下几个原因:

  1. 简洁的语法与解构赋值
  • 数组结构使得我们可以使用解构赋值轻松地获取当前状态和更新状态的函数,例如:
  • 这种写法简洁明了,不需要关心属性名称,可以自由命名变量。
jsx
const [count, setCount] = useState(0);
  1. 顺序性和约定
    • 数组的顺序是固定的,第一个元素永远是状态值,第二个元素永远是更新函数。
    • 这种固定的顺序保证了在组件多次渲染时,React 能够准确地关联到正确的状态和更新函数,而不会像对象那样需要额外的键来定位。
  2. 避免不必要的封装开销
  • 数组是一种轻量的数据结构。返回一个数组比返回一个对象开销更低,减少了额外的属性检查和命名空间的问题。
  1. 设计一致性
  • React 的设计理念中,Hooks 都是通过函数返回值来实现状态和行为的复用,返回一个固定结构的数组可以让 API 设计更加一致和直观。

总结来说,useState 使用数组而不是对象,主要是为了简化 API 使用,通过固定顺序和解构赋值提供更简洁、更高效的状态管理体验,同时保证了内部状态匹配的正确性和一致性。

5. React Hooks 解决了哪些问题?

React Hooks 在 React 16.8 引入后,带来了很多显著的改进,主要解决了以下几个问题:

  1. 组件逻辑复用问题

    • 以前的问题:在 class 组件中,要复用有状态逻辑通常依赖高阶组件(HOC)或者 Render Props,这种模式容易导致代码层级嵌套过深、逻辑分散,降低代码可读性和维护性。
    • Hooks 的解决方案:通过自定义 Hook,你可以将状态逻辑封装成函数,轻松在不同组件之间共享,避免了传统的 HOC 或 Render Props 模式,从而使组件逻辑更加清晰和复用性更高。
  2. 状态和副作用管理复杂性

    • 以前的问题:在 class 组件中,状态管理与生命周期函数紧密耦合,导致组件逻辑分散在不同的生命周期方法中,阅读和维护都较为困难。同时,副作用(如数据获取、订阅等)往往分散在多个生命周期方法中,容易产生混乱。
    • Hooks 的解决方案:Hooks(如 useStateuseEffect)允许你在一个函数组件中直接管理状态和副作用,将相关逻辑聚集在一起,使代码更加直观和模块化。useEffect 能够统一处理副作用,减少了生命周期函数之间的混乱。
  3. 简化组件的书写和理解

    • 以前的问题:class 组件涉及复杂的 this 指针绑定、生命周期方法以及较为冗长的语法,这给新手和维护人员带来较高的学习曲线和理解难度。
    • Hooks 的解决方案:Hooks 允许你使用函数组件来处理状态和副作用,语法上更简单、易于理解,并且不需要处理 this 绑定问题,使得代码更加清晰和易于测试。
  4. 更好的逻辑组织和代码结构

    • 以前的问题:class 组件的生命周期方法使得组件的逻辑分散在不同方法中,这会导致关注点不集中,难以对组件进行模块化和解耦。
    • Hooks 的解决方案:通过将相关逻辑封装到自定义 Hook 中,开发者可以按照功能模块组织代码,这样不仅提高了代码复用性,还使得单个组件内的逻辑更加集中和容易维护。

总结 React Hooks 通过引入一系列函数(如 useStateuseEffectuseContext 等):

  • 使得函数组件也能拥有状态和副作用管理的能力,
  • 解决了 class 组件在状态复用、逻辑组织、语法复杂性上的问题,
  • 让组件逻辑更加模块化、清晰、易于测试与维护。

这种设计极大地提升了 React 应用的开发效率和代码质量,同时也降低了新手的入门门槛。

6. React Hook 的使用限制有哪些?

React Hooks 使用时有一些严格的限制和规则,以确保其正常工作和状态的一致性。主要的限制包括:

  1. 只能在函数组件或自定义 Hook 中调用

    • 不能在普通的 JavaScript 函数、类组件或条件语句中调用 Hooks。
    • 只能在 React 函数组件或自定义 Hook(函数名称必须以 use 开头)中使用。
  2. 必须在组件的最顶层调用

    • Hooks 的调用顺序必须保持一致,不能在循环、条件或嵌套函数中调用,否则会破坏 React 内部的 Hooks 链表管理机制。
  3. 依赖项数组的正确使用(针对副作用 Hook)

    • 对于 useEffectuseCallbackuseMemo 等 Hook,必须正确声明依赖项数组,确保副作用或缓存值在依赖改变时正确更新,避免副作用执行不当或缓存数据错误。
  4. 不在组件外部共享状态

    • 每个组件实例都有自己独立的 Hook 状态,不要试图在多个组件之间直接共享 Hook 内部的数据,应该通过 propsContext 实现状态共享。

这些规则被称为 “Hooks 规则”,目的是让 React 能够准确地跟踪每个 Hook 的调用顺序,从而保证组件状态的正确更新和一致性。违反这些规则通常会导致 React 抛出错误或产生难以调试的状态问题。

7. useEffect 与 useLayoutEffect 的区别

useEffectuseLayoutEffect 都是用来处理副作用(side effects)的 Hook,但它们在执行时机和适用场景上有以下主要区别:

  1. 执行时机

    • useEffect

      • 在浏览器绘制(paint)后异步执行。
      • 不会阻塞浏览器更新,因此不会影响页面的渲染速度。
    • useLayoutEffect

      • 在所有 DOM 变更后、浏览器绘制前同步执行。
      • 会阻塞浏览器的绘制,直到副作用执行完毕。这类似于 class 组件中的 componentDidMountcomponentDidUpdate 中的同步执行
  2. 适用场景

    • useEffect
      • 适用于大多数副作用,比如数据获取、订阅、手动修改 DOM(非必须同步更新)以及设置定时器等。
      • 由于其不会阻塞绘制,推荐优先使用 useEffect 以提升用户体验。
    • useLayoutEffect
      • 适用于需要在浏览器绘制前同步读取或修改 DOM 的场景,比如测量 DOM 布局、同步触发重绘或动画等。
      • 当副作用的修改可能影响布局,并且你需要在浏览器呈现前完成这些修改时,使用 useLayoutEffect 更为合适。
  3. 性能与用户体验

    • useEffect
      • 异步执行,不会干扰浏览器的渲染流程,通常能提供更平滑的用户体验。
    • useLayoutEffect
      • 同步执行可能会导致浏览器在副作用执行期间出现卡顿,影响性能和用户体验,因此应谨慎使用,只在必要时使用。

总结

useEffectuseLayoutEffect
在绘制(paint)后执行在绘制(paint)前执行
不会阻塞渲染会阻塞渲染
适用于 API 请求、订阅、日志等适用于 DOM 操作、动画、布局计算
性能更好可能影响性能
  • 如果不确定,默认用 useEffect
  • 如果涉及 DOM 读取或修改(避免闪烁),用 useLayoutEffect
  • 优化关键帧动画、滚动操作等 UI 相关逻辑,使用 useLayoutEffect

🚀 建议:优先使用 useEffect,只有当 UI 需要同步调整时,才使用 useLayoutEffect

8. React Hooks在平时开发中需要注意的问题和原因

在平时的开发中,使用 React Hooks 时需要注意以下几个关键问题及其原因:

  1. 保持调用顺序的一致性

    • 问题:Hooks 必须在组件最顶层调用,不能放在条件语句、循环或嵌套函数中。
    • 原因:React 内部通过调用顺序来管理每个 Hook 的状态存储。如果调用顺序发生变化,可能导致状态混乱或错误。
  2. 只能在函数组件或自定义 Hook 中使用

    • 问题:Hooks 不能在普通 JavaScript 函数、类组件或组件外部调用。
    • 原因:Hooks 依赖于 React 的内部调度和状态管理机制,这些机制只在函数组件的上下文中存在。
    • 建议:确保所有 Hooks 调用都在函数组件或自定义 Hook 内部,并遵循命名规范(自定义 Hook 的名称必须以 use 开头)。
  3. 正确管理依赖数组

    • 问题:在使用 useEffectuseCallbackuseMemo 等时,依赖数组设置不当可能导致副作用重复执行或无法触发更新。
    • 原因:依赖数组决定了副作用函数的执行时机,遗漏或错误的依赖可能会导致 stale state 或不必要的重渲染。
    • 建议:仔细检查依赖数组,确保包含所有在副作用中使用且可能变化的变量;使用 ESLint 插件(如 eslint-plugin-react-hooks)帮助检测依赖问题。
  4. 避免不必要的状态更新和副作用

    • 问题:滥用 Hooks 可能会导致组件频繁重渲染或副作用执行,影响性能。
    • 原因:不恰当的状态管理或副作用逻辑可能导致不必要的渲染、重复的 API 调用或资源浪费。
    • 建议:利用 useMemouseCallback 缓存计算和函数,尽量优化性能;确保副作用只在必要时执行。
  5. 谨慎处理异步逻辑

    • 问题:在副作用中使用异步操作(如数据请求)时,需要考虑组件卸载后的清理问题。
    • 原因:异步操作可能在组件已经卸载后仍在执行,进而尝试更新状态,导致内存泄漏或报错。
    • 建议:在 useEffect 中返回一个清理函数,取消异步操作或订阅,确保组件卸载时能正确清理资源。
  6. 理解和遵循 Hooks 规则

    • 问题:开发者可能会忽视 Hooks 的规则,导致应用出现难以调试的错误。
    • 原因:Hooks 规则(比如只能在顶层调用、只能在函数组件中调用)是 React 用来保证状态一致性和内部管理正确性的重要保障。
    • 建议:熟悉并严格遵循官方的 Hooks 规则,使用 ESLint 插件来帮助检查代码。

通过注意以上问题,并遵循最佳实践,可以充分发挥 React Hooks 的优势,提高代码的可读性、可维护性和性能。

9. React Hooks 和生命周期的关系?

React Hooks 为函数组件提供了一种全新的方式来管理生命周期相关的逻辑,从而使得函数组件能够具备类似 class 组件中生命周期方法的能力。两者之间的关系可以这样理解:

  1. 统一管理副作用

    • class 组件生命周期:副作用代码通常分散在 componentDidMountcomponentDidUpdatecomponentWillUnmount 等方法中。
    • Hooks(useEffect):通过一个 Hook 就能管理副作用,在函数内部可以根据依赖数组的变化,分别处理挂载、更新和卸载时的逻辑。
      • 例如:
        • 没有依赖数组的 useEffect:每次渲染后都执行,类似于 componentDidUpdate(但也在初次挂载时执行)。
        • 空依赖数组:仅在组件挂载后执行一次(相当于 componentDidMount),并在返回的清理函数中处理卸载逻辑(相当于 componentWillUnmount)。
  2. 同步更新与布局测量

    • class 组件:有时需要在 DOM 更新后立即获取布局信息,通常使用 componentDidMountcomponentDidUpdate 来实现。
    • Hooks(useLayoutEffect):在浏览器绘制前同步执行副作用,适用于需要精确测量或操作 DOM 布局的场景,相当于 componentDidMount / componentDidUpdate 的同步版本。
  3. 简化组件逻辑组织

    • class 组件:生命周期方法往往将组件逻辑分散到多个方法中,容易导致逻辑分离和关注点不集中的问题。
    • Hooks:可以将相关副作用逻辑和状态管理放到一起,通过多个自定义 Hook 对逻辑进行模块化封装,保持代码清晰、易于维护。
  4. 无 this 指针问题

    • class 组件:生命周期方法中经常需要绑定 this 或处理上下文问题。
    • Hooks:函数组件没有 this,使得逻辑更加简单直观,开发者可以直接使用闭包来捕获变量,无需担心上下文绑定问题。

总结

  • 功能对应
    • useEffect 在大多数情况下可以替代 componentDidMountcomponentDidUpdatecomponentWillUnmount
    • useLayoutEffect 则用于需要同步执行的场景,类似于上述生命周期方法但在 DOM 更新后立即执行。
  • 优势
    • Hooks 让副作用、状态和其他逻辑可以在一个函数组件中以更加模块化的方式编写,而不是分散在不同的生命周期方法中。
    • 这种方式提高了代码的复用性、可读性和可维护性,同时降低了组件逻辑复杂度。

React Hooks 为函数组件带来了与生命周期方法同样强大的能力,同时在语法和设计上更加灵活和简洁,是 React 组件逻辑现代化的重要一步。

评论区

欢迎留言、补充或勘误。

xiaoba.blog