##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 的工作流程(简化版)
-
组件渲染时执行 Hook:每次组件渲染时,所有的 Hook 都会按照定义顺序执行一次。React 通过内部机制跟踪每个 Hook 的位置,并管理其状态和副作用。
-
管理状态变化和副作用:当你调用
useState
的状态更新函数时,React 会将状态更新存储起来,并在下一次重新渲染组件时更新状态。类似地,useEffect
的副作用会在组件渲染后执行。 -
依赖追踪与优化:
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 内部机制概述
-
每个组件都有独立的状态存储:
- React 会为每个组件实例分配一个独立的状态存储空间,用来保存这个组件的 Hook 状态(例如通过链表、数组或其他数据结构实现)。
- 每次调用
useState
或其他 Hook 时,React 都会根据当前组件的上下文,在对应的状态存储中取值或设置值。
-
渲染时与组件绑定:
- 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 状态
-
ComponentA
的状态:useState(0)
的初始值在ComponentA
的状态存储中。- 当用户点击
Increment A
按钮时,只会更新ComponentA
的countA
,不会影响ComponentB
。
-
ComponentB
的状态:useState(0)
的初始值在ComponentB
的状态存储中。- 当用户点击
Increment B
按钮时,只会更新ComponentB
的countB
,不会影响ComponentA
。
内部独立存储示意
React 会维护一个类似以下的结构:
const stateStorage = {
ComponentA: [{ state: 0 }], // ComponentA 的 hooks 状态
ComponentB