React 官方 Hook
Hook 是 React 16.8 之后引入的一个特性,允许你在不使用类组件的情况下在函数组件中引入状态和其他 React 特性。
useState
useState
允许在函数组件中引入局部状态。它的实现依赖于闭包和状态管理机制。- 每次渲染时,React 通过维护一个 “hook 调用栈” 来保存每次调用
useState
后的状态。这些状态被保存在一个链表结构中,每次渲染时 React 都会按顺序遍历这些状态。
useEffect
useEffect
允许在函数组件中执行副作用,比如数据请求、订阅、直接 DOM 操作等。- 它的实现原理是通过依赖数组来追踪变化,并在 DOM 完成更新后执行指定的副作用逻辑。如果某些依赖值发生变化,React 就会在下一次渲染后重新执行该副作用函数。
useContext
wip
useReducer
wip
useId
- useId – React
- generate a unique ID
- A component may be rendered more than once on the page—but IDs have to be unique! Instead of hardcoding an ID, generate a unique ID with useId.
Hook 的顺序与规则
- Hook 的使用顺序非常重要。React 需要在每次渲染时依赖 Hook 调用的顺序来关联正确的状态。所以,Hook 必须在顶层调用,不能放在条件或循环中。
参考
- React Hooks原理探究,看完不懂,你打我 - 掘金
- React Hooks解析(看这一篇就够了) | 骚客
- How hooks work | How React Works
- react hooks 的原理是什么 · Issue #138 · z-memo/interview
自定义hook
hook的理解
在 React 中被调用的且以 use 开头命名的函数叫 Hook。
- Hooks 是一种范式转换,从“生命周期和时间”的思维模式转变为“状态和与DOM的同步”的思维模式
准确来说,应该是逻辑片段复用。
和组件化思维不同,这是另外一个粒度更细的代码复用思维。例如我们之前提到的,获取同样的数据。在组件化思维中,一个完整的组件,包括了这份数据,以及这份数据在页面上的展示结果。因此这是不同的复用思维。
处理获取数据过程中的公用逻辑,处理公用的登陆逻辑等。自定义hooks封装的大多数情况下不是一个完整的页面逻辑实现,而是其中的一个片段。 自定义hook能够跟随函数组件重复执行,并且每次都返回最新结果。因此,我们可以非常放心大胆的封装异步逻辑。
- 自定义 Hook 是将逻辑封装为可重用的代码块。它实际上是一个普通的 JavaScript 函数,可以使用其他 Hook,并返回状态或副作用逻辑。自定义 Hook 让组件逻辑更加灵活和可重用。
自己的经历
在我写EuDs63/BookRecommend_Front: 图书推荐系统,就很经常需要从API获取数据。虽然这时候还没有自定义hook的概念,但很自然地封装了一个函数。在看了为什么你不应该在 React 中直接使用 useEffect 从 API 获取数据 | Sukka’s Blog,了解到用于数据请求的 React Hooks 库 – SWR。
发现了阿里巴巴开源的ahooks - React Hooks Library - ahooks 3.0,封装了常用的hook,代码量都不算很大,但都很巧妙,正在学习中。
看How to integrate Google Analytics with React JS application? | Saeloun Blog ,可以把ga给集成为hook,react-router-dom
读用于数据请求的 React Hooks 库 – SWR,看上了Subscription – SWR,感觉能用来替代项目中使用的redux,但搜了圈,能找到的资料不多,算了。
参考
- 超性感的React Hooks(五):自定义hooks
- 超性感的React Hooks(六)自定义hooks的思维方式
- 为什么你不应该在 React 中直接使用 useEffect 从 API 获取数据 | Sukka’s Blog
- Hook 规则 – React 中文文档
问题
React 是如何把对 Hook 的调用和组件联系起来的?
React 保持对当前渲染中的组件的追踪。Hook 只会在 React 组件中被调用(或自定义 Hook —— 同样只会在 React 组件中被调用)。
每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState() 调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState() 调用会得到各自独立的本地 state 的原因。
为什么useState是异步的
useState
的行为在某种程度上表现为异步,主要原因是为了优化 React 的渲染过程和性能。React 将多个状态更新合并到一起,并通过批量更新的方式避免不必要的重新渲染。具体原因如下:
- 批量更新机制
- React 在处理事件处理函数或生命周期方法(如
useEffect
)中的setState
调用时,通常不会立即应用状态更新,而是会将这些更新“批量”处理。在一次事件循环中,多个setState
调用会被合并为一次更新。这种批量更新策略可以提高性能,避免在每次状态变化后都重新渲染组件。 - 示例:批量更新
function MyComponent() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // 更新不会立即发生
setCount(count + 2); // 也不会立即发生
};
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
在这个例子中,虽然调用了两次 setCount
,但 React 会将它们合并为一次更新。
2. 异步特性防止多次渲染
如果
useState
是同步的,每次调用setState
时都会导致组件重新渲染,这样会严重影响性能。想象在一次事件处理函数中调用多次setState
,如果每次都导致同步重新渲染,性能开销将非常高。示例:同步更新的弊端
function MyComponent() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // 如果是同步,组件将立即重新渲染
setCount(count + 2); // 会导致多次无效渲染
};
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
如果 setCount
是同步的,React 会在每次状态更新时都重新渲染组件,导致性能下降。为了避免这种情况,React 通过异步机制将多次更新合并到一次渲染中。
React Fiber 架构
- React 引入了新的 Fiber 架构,使其能够以分片的方式执行任务,将大任务切分成小的可中断的子任务。在此过程中,React 可能会推迟状态更新,直到它完成当前的渲染工作。这样可以确保用户界面始终保持流畅。
- 如果
useState
是同步的,React 就无法利用 Fiber 架构的优势来进行任务调度和批量更新。
状态更新并不真正是异步的
- 虽然
useState
的更新机制表现为异步,但从技术上讲,它并不是真正的异步操作,像Promise
那样。实际上,它是在下一个渲染周期中同步更新的。React 将状态更新延迟到合适的时机,而不是立即应用。 - 示例:获取最新状态
function MyComponent() { const [count, setCount] = useState(0); const handleClick = () => { setCount(prevCount => prevCount + 1); // 使用回调函数获取最新的状态 setCount(prevCount => prevCount + 1); // 这样可以确保多次更新正确执行 }; return ( <div> <p>{count}</p> <button onClick={handleClick}>Increment</button> </div> ); }
通过回调函数
prevCount
,我们可以确保每次更新都基于最新的状态值。这样即便setState
是异步的,也能确保状态更新的正确性。- 虽然
为什么hooks不能写在if/else等语句里
Hooks 调用顺序的依赖
- React 使用一个内部的“链表”来跟踪组件中的每个 Hook 的调用。当 React 渲染组件时,Hooks 是按照定义的顺序被调用的。在后续渲染中,React 依赖这个顺序来确保同一个 Hook 对应的是之前的状态或副作用。
- 示例
function MyComponent() { const [count, setCount] = useState(0); // Hook 1 const [name, setName] = useState(''); // Hook 2 useEffect(() => { // Hook 3 document.title = `Count: ${count}`; }, [count]); return <div>{count}</div>; }
在每次渲染中,React 会按照调用顺序分配状态:
useState(0)
对应第一个状态count
。useState('')
对应第二个状态name
。useEffect
处理副作用。
如果这个顺序保持一致,React 能够正确地追踪状态和副作用。
在条件语句中使用 Hooks的后果
可能会导致 Hooks 的调用顺序在不同渲染中不一致
错误示例
function MyComponent() { const [count, setCount] = useState(0); // Hook 1 if (count > 0) { const [name, setName] = useState(''); // Hook 2 (条件性调用) } useEffect(() => { // Hook 3 document.title = `Count: ${count}`; }, [count]); return <div>{count}</div>; }
如果
count > 0
,name
的useState
会被调用。但如果count <= 0
,该useState
不会被调用。这导致:- 当
count > 0
时,useEffect
是第三个 Hook。 - 当
count <= 0
时,useEffect
是第二个 Hook。
这样,React 无法知道哪个 Hook 应该关联到哪个状态,状态更新和副作用可能会错误地关联到不同的 Hook,导致不可预知的错误。
React 如何追踪 Hooks
- React 通过一个全局的“调用栈”来追踪每个组件中的 Hooks。当组件渲染时,React 会按顺序调用所有的 Hooks,并在内部记录这些调用。这样,每次渲染时,React 能够依赖这个顺序来关联状态。
- 如果在
if/else
中使用 Hooks,可能会导致某些 Hooks 在特定条件下被跳过,破坏这个顺序,从而导致错误的状态关联。
报错提示
Rendered more hooks than during the previous render
Hooks can only be called inside the body of a function component.
参考
hooks能实现类里面的所有生命周期吗
- “Commit phase"中的三个方法,Can work with DOM, run side effects, schedule updates:
componentDidMount
-> the useEffect hook with the second argument of []componentDidUpdate
-> the useEffect hook with the second argument of [state]componentWillUnmount
-> return a function that runs on unmounting inside the useEffect function,例:
useEffect(() => { console.log("Hello"); return () => { console.log("Bye"); }; }, []);
- 上面的类比只是类比,不能等同,原因:
- 实际原理不同
- 执行时机不同