• 首页

  • 写作

  • 文章归档

  • 照片

  • 友情链接

  • 旅行

  • 读书

  • 日志

  • 随记

  • 人文历史

  • linux

  • 前端
b l o g
b l o g

admin

lzp

01月
06
前端
react ui

React 的 Hook 系统 的工作原理

发表于 2025-01-06 • 字数统计 7546 • 被 10 人看爆

##2024/10/11 02:21:50:

React 的 Hook 系统 的工作原理?

写在前面: 具体的工作机制,看文字只能了解个模糊大概,想要更清晰,有个代码,简化的代码,或伪代码更好,更能理解清楚,在理解程序方面,文字不是表达逻辑的最佳工具,图像会好一点,代码是将三维的逻辑以二维的形式具体表达出来。文字只能算一维逻辑。

React 的 Hook 系统是为了在函数组件中提供类似于类组件的状态管理、生命周期控制等功能。Hooks 允许你在函数组件中使用 React 的核心功能(如状态和副作用),而无需编写类组件。

@@@ 核心的 React Hook 工作原理

React Hook 的工作原理基于闭包、状态管理以及依赖追踪。通过 Hook,React 可以在函数组件的每次渲染中保留某些值(如状态或副作用)。下面是一些核心原理和机制:

@@@@ 1. 闭包与状态保存

  • 当你使用 useState 或 useReducer 等 Hook 时,React 为组件创建了一组内部状态,并将其与组件的渲染过程绑定。
  • Hook 使用闭包来访问当前的状态和其他值。每次组件重新渲染时,React 会保持这些状态的“快照”。
  • 例如,当你在函数组件中调用 useState 时,它返回的状态值在组件的每次渲染之间是保持的。更新状态时,React 会重新渲染组件,并重新计算状态。
const [count, setCount] = useState(0); // useState 是如何工作的?

useState 会记录 count 的初始值,并且 setCount 会触发状态更新和重新渲染。在每次渲染中,count 都是最新的。

@@@@ 2. 依赖追踪与副作用(useEffect)

  • useEffect 是一个用于处理副作用的 Hook。副作用是指那些不直接产生 UI 的操作,比如网络请求、订阅、手动操作 DOM 等。
  • useEffect 的内部工作原理基于依赖项数组。React 在组件的第一次渲染后,执行 useEffect 回调,然后在依赖项变化时重新执行。如果没有依赖项,useEffect 每次组件重新渲染都会执行。
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 依赖项是 count,只有当 count 变化时,才会重新运行 effect。

React 通过比较依赖项的值是否改变,决定是否重新执行副作用。

@@@@ 3. 状态与渲染的同步

  • 在 React 中,状态的变化会导致组件重新渲染。React 通过调度机制确保状态变化后触发重新渲染,而不是立即执行状态变更。在一次渲染过程中,多次 setState 调用会被批处理,以提高性能。
  • 当你调用 useState 或其他状态 Hook 时,React 会为每次状态更新分配唯一的索引来跟踪状态。每次渲染时,React 会使用这个索引在 Hook 调用中正确地管理和恢复状态。
const [count, setCount] = useState(0);

const increment = () => {
  setCount(prevCount => prevCount + 1);
};

每次调用 setCount,React 会安排一个更新过程。更新完成后,组件会重新渲染,count 会被更新为新的值。

@@@@ 4. Hook 调用顺序

  • React 使用一个规则来确保 Hook 的正确执行顺序——Hook 只能在组件的顶层调用,不能在循环或条件语句中调用。这是因为 React 依赖 Hook 的调用顺序来确定状态和副作用的管理。
// 这是错误的用法
if (condition) {
  const [value, setValue] = useState(0); // 不能在条件中调用 Hook
}

React 通过调用顺序来识别不同的状态。如果 Hook 的调用顺序在不同的渲染中不一致,React 将无法正确地管理状态。

@@@@ 5. useRef 和 useMemo 等优化 Hook

  • useRef 提供了访问 DOM 节点或保持组件生命周期内持久变量的能力,且不会引起重新渲染。useRef 的值在渲染过程中保持不变,并且不会触发重新渲染。
  • useMemo 和 useCallback 用于性能优化,它们会在依赖项没有改变的情况下缓存计算结果或函数引用,以减少不必要的重新计算或函数重新定义。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); // 仅在 a 或 b 变化时重新计算

@@@ Hook 的工作流程(简化版)

  1. 组件渲染时执行 Hook:每次组件渲染时,所有的 Hook 都会按照定义顺序执行一次。React 通过内部机制跟踪每个 Hook 的位置,并管理其状态和副作用。

  2. 管理状态变化和副作用:当你调用 useState 的状态更新函数时,React 会将状态更新存储起来,并在下一次重新渲染组件时更新状态。类似地,useEffect 的副作用会在组件渲染后执行。

  3. 依赖追踪与优化:useEffect、useMemo、useCallback 等 Hook 会根据依赖项的变化决定是否执行,这样可以避免不必要的计算或副作用执行。

@@@ 小结

React Hook 系统的工作原理依赖于状态管理、闭包和依赖追踪,确保状态和副作用能够在函数组件中灵活且高效地管理。通过 Hook,React 实现了函数组件的状态持久化和生命周期管理,而无需使用类组件的复杂逻辑。

以上是chatgpt回答,不清楚

解释一下代码的意思, 以及currentHookIndex 的作用

原文链接: https://juejin.cn/post/7409904300854444058

let currentHookIndex = 0;
const hooks = [];

function useState(initialValue) {
  const hook = hooks[currentHookIndex] || { state: initialValue, queue: [] };
  hooks[currentHookIndex] = hook;
  currentHookIndex++;

  hook.queue.forEach(action => {
    hook.state = action(hook.state);
  });
  hook.queue = [];

  const setState = (action) => {
    hook.queue.push(action);
  };

  return [hook.state, setState];
}

function useEffect(effect, deps) {
  const hook = hooks[currentHookIndex] || { deps: undefined };
  const hasChanged = deps ? deps.some((dep, i) => !Object.is(dep, hook.deps[i])) : true;
  if (hasChanged) {
    effect();
  }
  hook.deps = deps;
  hooks[currentHookIndex] = hook;
  currentHookIndex++;
}


function MyComponent() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log('Effect ran');
  }, [count]);

  if (count > 5) {
    const [extra, setExtra] = useState(0); // 会破坏调用顺序
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

这段代码实现了一个类似于 React 的 useState 和 useEffect 的 Hook 系统,但它是简化版本。我们来一步步解释它的作用,尤其是 currentHookIndex 的角色。

@@@ 代码解释:

@@@@ 1. Hooks 存储与 Hook Index:

let currentHookIndex = 0;
const hooks = [];
  • currentHookIndex 是一个全局变量,用于跟踪当前组件执行到哪个 hook。
  • hooks 是一个数组,用来存储每个 Hook(如 useState 或 useEffect)的状态。每个组件中的多个 Hooks 会按照调用顺序存储在这个数组中。

@@@@ 2. useState(initialValue):

function useState(initialValue) {
  const hook = hooks[currentHookIndex] || { state: initialValue, queue: [] };
  hooks[currentHookIndex] = hook;
  currentHookIndex++;
  
  hook.queue.forEach(action => {
    hook.state = action(hook.state);
  });
  hook.queue = [];
  
  const setState = (action) => {
    hook.queue.push(action);
  };
  
  return [hook.state, setState];
}
  • useState 函数实现了状态管理。每次调用时:
  • 它首先从 hooks[currentHookIndex] 取出当前 hook,如果没有(说明是第一次调用),它会初始化一个具有 state 和 queue 属性的对象。
  • 然后把该 hook 存回 hooks 数组的相应位置。
  • 使用 currentHookIndex++ 来递增索引,确保下一个 Hook 调用时,取到正确的位置。
  • hook.queue 存储了可能等待执行的状态更新操作。每次渲染时,会将队列中的函数依次应用到 hook.state 上,更新状态。
  • setState 函数接收一个操作函数,将它添加到 queue 中,等待下一次渲染时处理。

@@@@ 3. useEffect(effect, deps):

function useEffect(effect, deps) {
  const hook = hooks[currentHookIndex] || { deps: undefined };
  const hasChanged = deps ? deps.some((dep, i) => !Object.is(dep, hook.deps[i])) : true;
  if (hasChanged) {
    effect();
  }
  hook.deps = deps;
  hooks[currentHookIndex] = hook;
  currentHookIndex++;
}
  • useEffect 模拟了 React 的 useEffect。它用于在组件渲染之后执行副作用,比如数据请求或 DOM 操作。
  • 同样地,先从 hooks[currentHookIndex] 获取当前 hook 对象,如果不存在就初始化。
  • 它检查 deps(依赖数组)中的值是否发生了变化。如果 deps 中的值发生了变化(或是 deps 未定义),就会执行传入的 effect 函数。
  • 执行完后,将 deps 保存下来,更新到 hook 中。

@@@@ 4. MyComponent():

function MyComponent() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log('Effect ran');
  }, [count]);

  if (count > 5) {
    const [extra, setExtra] = useState(0); // 会破坏调用顺序
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
  • MyComponent 是一个模拟的 React 组件。
  • 通过 useState(0) 初始化 count 为 0,并定义 setCount 函数用于更新 count。
  • 通过 useEffect 实现了副作用,当 count 改变时会执行 console.log('Effect ran')。
  • 当 count > 5 时,代码中又调用了一个新的 useState。然而,这里有一个潜在问题,因为 useState 的调用顺序依赖于 currentHookIndex。如果根据条件动态添加 Hook(比如 count > 5 时),会打破 Hook 的调用顺序,使得 currentHookIndex 和 hooks 数组之间不再同步,导致错误的状态管理。

@@@ currentHookIndex 的作用:

  • currentHookIndex 用来跟踪当前组件已经调用的 Hook 次数,并确保每次调用 useState 或 useEffect 时,都能获取并更新正确的状态。
  • 每次函数式组件渲染时,Hook 的调用顺序必须保持一致(即每次渲染期间,Hook 的调用顺序不能被条件语句打乱)。否则,currentHookIndex 会偏离,导致错误的状态或副作用执行。

currentHookIndex 什么时候会归零?

currentHookIndex 在每次组件重新渲染时应归零。这是因为在 React 或任何类似的框架中,函数式组件每次渲染时都会重新调用函数,而 Hook 必须按固定的顺序重新执行。

在你的代码中,currentHookIndex 是一个全局变量,它随着每次调用 useState 或 useEffect 递增,用来确保每个 Hook 获取正确的状态。但为了确保在每次组件渲染时从头开始计数,currentHookIndex 必须在组件每次渲染前重置为 0。

@@@ 归零的时机

  • 在每次组件渲染之前,currentHookIndex 应该被重置为 0。这意味着每当 MyComponent 函数被调用时,currentHookIndex 需要重新从 0 开始计数,确保从头开始注册每个 Hook 的状态。

@@@ 解决方法
你需要在组件渲染时手动重置 currentHookIndex,像这样:

function MyComponent() {
  currentHookIndex = 0;  // 每次渲染时重置 currentHookIndex

  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log('Effect ran');
  }, [count]);

  if (count > 5) {
    const [extra, setExtra] = useState(0); // 会破坏调用顺序
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

@@@ 为什么要这样做?
在 React 中,函数组件每次渲染时,Hooks 都是按照固定的顺序执行的,因此必须保证 useState、useEffect 等 Hooks 的执行顺序和上次渲染时相同。通过在每次渲染开始时将 currentHookIndex 重置为 0,确保下次渲染时从第一个 Hook 开始正确获取状态。

@@@ 如果不归零会发生什么?
如果 currentHookIndex 不归零,那么在每次组件重新渲染时,useState 和 useEffect 等 Hook 会从上次停留的地方继续,因此会访问到错误的状态或副作用。例如:

  • Hook 会取到错误的 state 或 deps,导致应用逻辑出错。
  • 动态添加 Hook(比如在条件语句中使用 useState)会进一步打乱索引顺序,导致严重的逻辑错误。

每一个 组件都有一个独立的 hooks 吗?

是的,每个 React 组件都维护自己独立的 hooks 数据结构。这是 React 内部的一种机制,用来确保不同组件的状态和 Hook 调用不会互相干扰。

React 内部机制概述

  1. 每个组件都有独立的状态存储:

    • React 会为每个组件实例分配一个独立的状态存储空间,用来保存这个组件的 Hook 状态(例如通过链表、数组或其他数据结构实现)。
    • 每次调用 useState 或其他 Hook 时,React 都会根据当前组件的上下文,在对应的状态存储中取值或设置值。
  2. 渲染时与组件绑定:

    • React 渲染每个组件时,会根据组件的调用顺序分配状态和更新队列。
    • 如果组件重新渲染,它会取回之前存储的状态,而不是影响其他组件。

举例说明

假设有两个组件 ComponentA 和 ComponentB,它们都使用了 useState:

function ComponentA() {
  const [countA, setCountA] = useState(0);

  return (
    <div>
      <p>Count A: {countA}</p>
      <button onClick={() => setCountA(countA + 1)}>Increment A</button>
    </div>
  );
}

function ComponentB() {
  const [countB, setCountB] = useState(0);

  return (
    <div>
      <p>Count B: {countB}</p>
      <button onClick={() => setCountB(countB + 1)}>Increment B</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <ComponentA />
      <ComponentB />
    </div>
  );
}

组件独立的 Hook 状态

  1. ComponentA 的状态:

    • useState(0) 的初始值在 ComponentA 的状态存储中。
    • 当用户点击 Increment A 按钮时,只会更新 ComponentA 的 countA,不会影响 ComponentB。
  2. ComponentB 的状态:

    • useState(0) 的初始值在 ComponentB 的状态存储中。
    • 当用户点击 Increment B 按钮时,只会更新 ComponentB 的 countB,不会影响 ComponentA。

内部独立存储示意

React 会维护一个类似以下的结构:

const stateStorage = {
  ComponentA: [{ state: 0 }], // ComponentA 的 hooks 状态
  ComponentB

分享到:
基于 vite react tailwind 项目并部署到 cloudflare
build tailwind css 项目
  • 文章目录
  • 站点概览
admin

! lzp

hello

Github Twitter QQ Email Telegram RSS
看爆 Top5
  • 历史与人文 视频链接 189次看爆
  • 2022日志随笔 175次看爆
  • 我的青海湖骑行 164次看爆
  • 读书随笔 124次看爆
  • rs2 设置教程 97次看爆

站点已萌萌哒运行 00 天 00 小时 00 分 00 秒(●'◡'●)ノ♥

Copyright © 2025 admin

由 Halo 强力驱动 · Theme by Sagiri · 站点地图