1.React hooks
React hooks是React16.8的新特性,可以让React函数组件具有状态,并提供类似componentDidMount和componentDidUpdate等生命周期方法。
React 早期版本,类组件可以在shouldComponentUpdate中,通过判断props和state是否发生变化来决定需不需要重新渲染组件。而继承PureComponent后,当组件更新时,如果组件的 props 和 state 都没发生改变, render 方法就不会触发,省去 Virtual DOM 的生成和比对过程,达到提升性能的目的。具体就是 React 自动在shouldComponentUpdate中帮我们做了一层浅比较。这样我们可以少写 shouldComponentUpdate 函数。相对于函数组件,我们知道函数每次调用,就会执行函数体,然而React 官网没有提供对应的方法来缓存函数组件以减少一些不必要的渲染,直到 16.6 出来的 React.memo函数。
React 16.8 新出来的Hooks可以让React 函数组件具有状态,并提供类似 componentDidMount和componentDidUpdate等生命周期方法。
Hook 这个单词的意思是"钩子"。
React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。 React Hooks 就是那些钩子。
你需要什么功能,就使用什么钩子。React 默认提供了一些常用钩子,你也可以封装自己的钩子。
所有的钩子都是为函数引入外部功能,所以 React 约定,钩子一律使用use前缀命名,便于识别。你要使用 xxx 功能,钩子就命名为 usexxx。
2.useState
鉴于纯函数自身无法保存状态,要在两次组件函数执行期间保存和修改状态,必须引入副作用,使用外部存储能力,所以引入了useState。
const [state, setState] = useState(initialState);
其中,初始化参数initialState可以试一个值,也可以是一个函数。
如果传递函数作为 initialState
,则它将被视为 初始化函数。它应该是纯函数,不应该接受任何参数,并且应该返回一个任何类型的值。当初始化组件时,React 将调用你的初始化函数,并将其返回值存储为初始状态。
set函数的参数可以是一个值nextState,也可以是一个函数。如果你将函数作为 nextState
传递,它将被视为 更新函数。它必须是纯函数,只接受待定的 state 作为其唯一参数,并应返回下一个状态。React 将把你的更新函数放入队列中并重新渲染组件。在下一次渲染期间,React 将通过把队列中所有更新函数应用于先前的状态来计算下一个状态。
2.1 必须
在组件的顶层调用
第一个关键点:useState
是一个 Hook,因此你只能在 组件的顶层 或自己的 Hook 中调用它。你不能在循环或条件语句中调用它。如果你需要这样做,请提取一个新组件并将状态移入其中。
因为 hooks 为了在函数组件中引入状态,维护了一个有序表。
比如第一次执行函数组件时,我们拿到状态 count(通过useState,初始值为 0 )和 bool(通过 useState,初始值为 false),它们其实被保存到一个有序表中,它们的值会记录下来: [0, false]。
第二次执行函数组件, 会 按顺序 从这个表中拿出 0 和false,赋值给 count 和 bool。
如果你把 hook 写到判断条件下,导致某个 useState 不执行了,这里我们假设 count 的 useState 因为判断条件没有执行,会发生什么?结果是 bool 拿到了 0,发生了错位。
函数本身不能保存状态,我们需要额外维护一个有序的表,在执行 useState 之类的 hook 时,将它们保存到这个表里。
这要求每次函数组件的 hook 执行的位置相同,数量正确,否则会导致错位,不能拿到预期的状态值。
2.2 严格模式下的多次执行
第二个关键点:在严格模式中,React 将 两次调用组件函数、初始化函数和更新函数,以 帮你找到意外的不纯性。这只是开发时的行为,不影响生产。如果你的初始化函数是纯函数(本该是这样),就不应影响该行为。其中一个调用的结果将被忽略。
调用setState只会将更新函数放入队列,并不会立即修改当前的state,直到下次渲染的时候,state才会修改为更新后的值。那么如果你的setState是纯函数,在两次渲染之间无论执行多少次,都会有相同的state。反之,如果你的setState不纯,直接修改了当前的state,那么多次执行后的结果都不同。React使用Object.is 比较两个state是否相同。
下面是一个纯函数的例子:
function TodoList() {
// 该函数组件会在每次渲染运行两次。
const [todos, setTodos] = useState(() => {
// 该初始化函数在初始化期间会运行两次。
return createTodos();
});
function handleClick() {
setTodos(prevTodos => {
// 该更新函数在每次点击中都会运行两次
return [...prevTodos, createTodo()];
});
}
// ...
更新函数不纯的例子:
setTodos(prevTodos => {
// 🚩 错误:改变 state
prevTodos.push(createTodo());
});
参考: