Hook
概览
Hook 是 React 16.8 版本引入的一种新特性,它允许我们在函数组件中使用 state 和其他的 React 特性,而不需要编写 class。更加灵活和强大,提高了代码复用性。
Hooks 的底层实现主要依赖于 React 的 Fiber 架构 和 JavaScript 的闭包。闭包可以保存 Hook 的状态和内部变量。
可以使用内置的 Hook 或自定义 Hook。
推荐安装 eslint-plugin-react-hooks
Hook 规则
- 只能在函数组件的最顶层调用 Hook
- 不能在循环、条件判断或嵌套函数中调用 Hook
- 不能在普通的 js 函数中调用 Hook
useState
useState
用于在函数组件中添加状态。接受一个初始值作为参数,返回一个由当前状态和更新状态的函数组成的数组。
原理:依赖闭包和 Hook 链表管理状态,默认异步批量更新。
- 闭包:useState 返回一个状态值和一个更新函数,这两者通过闭包与组件的当前状态绑定在一起。
- fiber 架构:在 React 内部,每个函数组件都有一个对应的 fiber 节点。useState 的状态存储在该 fiber 节点的 memoizedState 属性中,具体表现为一个 hook 对象。
- 更新机制:当调用 useState 返回的更新函数(如 setCount)时,React 并不会立即修改状态,而是将更新操作加入到一个队列中。在下一次组件渲染时,React 会处理这个队列,应用更新后的状态值,然后重新渲染组件。
简单来说,useState 通过 hooks 机制和 fiber 架构,实现了状态的持久化和更新管理。
import { useState } from 'react';
export default function Home() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(prev => prev + 1);
};
return (
<div>
<p>{count}</p>
<button onClick={increment}>increment</button>
<button onClick={() => setCount(count - 1)}>decrement</button>
</div>
);
}
useState 针对每个组件实例都有自己的状态,不会共享状态。
如果状态是对象或数组,应该替换状态而不是更改现有对象,参考 不可变数据。
const initialState = [
{ id: 0, title: 'a', done: true },
{ id: 1, title: 'b', done: false },
{ id: 2, title: 'c', done: false }
];
const [list, setList] = useState(initialState);
// 向数组中添加元素
setList([...list, { id: 3, title: 'd', done: false }]);
// 删除数组中的某个元素
setList(list.filter(item => item.id !== id));
// 更新数组中的某个元素
setList(
list.map(item => {
if (item.id === newItem.id) {
return newItem;
} else {
return item;
}
})
);
const [state, setState] = useState({ id: 0, count: 0 });
// 更改对象状态的值,不能直接使用 state.count = 1
setState({ ...state, count: 1 });
1、为什么要使用 useState,直接声明变量不行吗?
假设直接声明变量,点击按钮后能看到 count 打印出来的值是变了,但是页面却没有变化。因为没有触发组件重新渲染,详见 React 渲染原理。
export default function Home() {
let count = 0;
const increment = () => {
count = count + 1;
console.log(count);
};
return (
<div>
<p>{count}</p>
<button onClick={increment}>click</button>
</div>
);
}
2、为什么 setCount(count + 1)
和 setCount(prev => prev + 1)
都可以实现功能?
在 React18+,useState 返回的更新状态的函数 setCount 是一个异步函数,即使在连续多次调用时,也不能保证它们的值是最新的。因此,如果在调用 setCount 函数时,需要使用先前状态的值来计算新状态,那么使用回调函数的方式会更可靠。
在 setCount(count + 1)
的写法中,count 的值依次加一。但是,由于 setCount 是异步函数,实际上可能存在多次点击只触发一次更新的情况,此时计算新状态的值 count + 1
就会出现问题。
相比之下,setCount(prev => prev + 1)
的写法更可靠。在这种写法中,使用回调函数的方式来计算新状态的值,这个回调函数的参数 prev 是当前状态的值,可以保证它是最新的。因此,无论 setCount 函数是否被合并,都可以正确地计算新状态的值。
综上所述,虽然 setCount(count + 1)
在某些情况下可以正常工作,但是使用回调函数的方式 setCount(prev => prev + 1)
更可靠,建议在使用 useState 时采用这种写法。
3、为什么说组件状态的更新是异步的?
在下面的例子中,点击一次后,页面上显示 1,但是打印出来的还是 0。如果是同步的,执行setCount((prev) => prev + 1)
会将 count 的值变为 1,那么之后的打印结果就应该是 1,因此组件状态的更新是异步的。
import { useState } from 'react';
export default function Home() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(prev => prev + 1);
console.log(count);
};
return (
<div>
<p>{count}</p>
<button onClick={increment}>increment</button>
</div>
);
}
4、异步批量更新
React 18+ 默认为异步批量更新,除非使用 flushSync
强制同步。
示例 1:点击后 count 变为 1,而不是 2
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
console.log(count); // 0
};
return <button onClick={handleClick}>{count}</button>;
}
示例 2:使用函数式更新,点击后 count 变为 2
setCount(prev => prev + 1); // 基于最新状态更新
setCount(prev => prev + 1); // 再次基于最新状态
惰性初始化的值
使用惰性初始值,避免初始值重复计算。传递计算函数本身,而不是函数的计算结果。
下面示例中,假如有一个初始值是经过复杂计算得来的,如果直接将计算结果传给 useState,那么在每次点击按钮时,都会重新计算一次初始值。可以在 useState 里传一个函数,这个函数会返回初始值,这样就避免 了重复计算
import { useState } from 'react';
function initialState(n) {
console.log(123);
return n + 1 + 2 + 3; // 假设计算很昂贵
}
function App() {
// 1. 返回函数的计算结果
// const [count, setCount] = useState(initialState(0));
// 2. 返回计算函数本身
const [count, setCount] = useState(() => initialState(0));
return (
<>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>click</button>
</>
);
}
export default App;
useEffect
useEffect
是执行副作用操作的。副作用是指与组件渲染无关的操作,如修改全局变量、进行网络请求、手动操作 DOM、设置定时器等。
useEffect(callback, dependencies)
,第一个参数是要执行的函数,第二个参数是可选的依赖项数组。
关于依赖项参数的说明:
- 无参数,表示每次渲染的时候都执行,即任一 state 更新即执行
- 空数组,表示挂载执行,只执行一次
- 加
[]
并且里面有变量,表示变量更改了就执行,初始的时候就执行
如果effect
返回一个回调函数,React 将会在执行清除操作时调用它,即组件销毁时执行。
import { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count更新了即执行');
return () => console.log('清除时调用');
}, [count]);
return (
<>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>click me</button>
</>
);
}
假设做一个数值自增功能:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
return <h1>{count}</h1>;
}
当count
每次改变时,定时器都重新设定和清除。更好的方案是使用函数式更新,如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。这样effect
只会执行一次,但是仍能实现自增功能。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
1、如果在 useEffect 里使用了某些变量,但是没有在依赖项中指定,会发生什么?
监听不到变量值的变化。如下,只会打印一次 count 的值且为 0
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count);
console.log('useEffect');
}, []);
return <button onClick={() => setCount(count + 1)}>add</button>;
};
2、在 useEffect 中使用的 setCount() 是一个函数,本质上也是一个局部变量,为什么它不需要在依赖项中指定?
由 useState 返回的函数在整个组件生命周期中是稳定的,setCount 函数的引用不会改变,不需要添加到依赖数组中。
hooks 的依赖
在 useEffect、useMemo、useCallback 中,都有依赖项。
依赖项中定义的变量一定是在回调函数中使用的,一般是一个数组。
react 会浅比较依赖项是否发生了变化,要注意数组和对象。例如下面的例子,会在每次改变 count 时都执行 useEffect 的回调函数,因为每次点击都会创建一个新对象,导致依赖项发生变化。
import { useEffect, useState, useMemo } from 'react';
const App = () => {
const [count, setCount] = useState(0);
const obj = [{ a: 1 }];
useEffect(() => {
console.log('useEffect');
}, [obj]);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>add</button>
</div>
);
};
export default App;
这里可以用 useMemo 缓存结果:const obj = useMemo(() => [{ a: 1 }], []);
useReducer
useReducer
是useState
的替代品。接收一个形如(state, action) => newState
的 reducer
,并返回当前的state
和dispatch
方法。
通过action
的传递,更新复杂逻辑的状态
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
function Home() {
// state dispatch 可以自定义名称
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div className="home">
<h1>{state.count}</h1>
<button onClick={() => dispatch({ type: 'decrement' })}>decrement</button>
<button onClick={() => dispatch({ type: 'increment' })}>increment</button>
</div>
);
}
export default Home;