学习材料为 react 文档。
1. Hook 介绍
Hook 的意思是把 state 和生命周期函数用“钩子”钩过来,让函数组件也能使用 state。以及提供其他各种特性使得函数组件比 class 组件更好用,以解决 class 组件的各种问题。比如说:
- 组件复用:class 如果要复用一些状态逻辑,则需要用到 HOC,render props 等概念,这使得组件嵌套很复杂,难以阅读。
- 生命周期函数的分离:比如说在
componentDidMount里设置了一个计时器,却要在componentWillUnmount里删除这个计时器。逻辑被分离到两个函数中。而且如果组件行为比较复杂的话,会在componentDidMount里定义很多不相干的逻辑,同时在componentWillUnmount里继续这些逻辑。 (注:hook 指的不是函数组件本身,而是一些专门提供给函数组件的 JS 函数,以提供各种新特性)
2. state hook
也就是 useState。
- useState 用于代替 this.state 和 this.setState。
- useState 接受唯一参数,这个参数会作为声明的 state 变量的初始值。可以是任意数据类型。
- useState 返回两个值,第一个为声明的 state 变量,第二个为改变这个变量的方法。
// 声明一个叫做 myState 的 state 变量,之后只能使用 setMyState 来改变这个 state。
// 获取值使用了数组解构语法
const [myState, setMyState] = useState("初始值");
// 直接使用 myState 来获取 state 变量
<h1>{myState}</h1>
// 使用 setMyState 改变变量,也很简单
setMyState("改变后的值");
// 传入一个函数,可以获取修改前的 prevState 参数,返回值为新的 state 值
setMyState((prevState) => {
return (prevState + 1);
});
hook 的 setXXX 是替换,而不是 class 组件的 setState 的合并。
这个听起来像是明摆的事情,每一个 state 只对应一个变量可不就只有替换嘛。
但是当你给一个 state 变量赋值为对象。更改这个对象的某一两个属性的时候,你可能会错用成 setState 的方式只写出要更改的对象。但这样是错的,你应该写出所有的属性。
const [myState, setMyState] = useState({name: "alice", age: 14});
setMyState({name: "no one"}) // 错误
setMyState({name: "no one", age: 14}); // 正确
3. effect hook
useEffect(),可以理解为 componentDidMount, componentDidUpdate, componentWillUnmount 的结合。
3.1 无需清除副作用的用法
useEffect() 接受一个函数作为参数,会在每次渲染之后执行传入的函数,无论是挂载后还是更新后,即相当于一次同时定义componentDidMount和 componentDidUpdate。
// 在第一次渲染和之后每次渲染时执行
useEffect(() => {
/*处理数据等*/
});
在 useEffect() 里使用匿名函数(该函数被称为 effect)。由于该函数是在 useEffect() 里创建,这样会使每次都要重新创建 effect,但是这是刻意而为的。因为每次重新生成 effect 意味着每次执行 useEffect() 的时候都会获取最新的 state。
3.2 需要清除副作用的用法
有时候,useEffect() 里会做一些之后需要被清除的工作,比如订阅外部数据,设置计时器等。都需要被清除。在 class 组件中往往在 componentWillUnmount 中执行清除。
而在函数组件中,这个工作通过 useEffect() 返回的函数负责。这样子,订阅和清除的逻辑就全部放在同一个函数中。而不是像生命周期函数要写在两个函数中。
useEffect(() => {
/*处理数据等*/
return (() => {
/*清除工作*/
})
})
每次重新渲染的时候,清除函数都会先于该 effect 执行一次(第一次渲染不执行),而不是只有当组件被卸载的时候执行一次。这是为了解决一个常见的 bug。
以下是运行顺序,以及一个示例。
// 装载
运行 useEffet(),不运行清除函数
// 更新
先运行清除函数,再运行 useEffect()。
// 这里有一个 setTimeout 显示时钟的用法示例
function Clock(props) {
const [date, setDate] = useState(new Date()); // 设置 state
useEffect(() => {
const timer = setTimeout(() => {setDate(new Date());}, 100); // 每次 timeout 到达的时候执行 setDate,组件重新渲染
// 这时候因为是更新,执行清除函数
// 但是 setTimeout 已经执行完了自行清除了
// 清除函数执行了个寂寞,然后执行useEffect
// 设置下一个 setTimeout
// timeout 到达,执行 setDate,组件重新渲染
// 循环往复
return () => {
clearTimeout(timer);
}
})
return (<div>{date.toTimeString()}</div>)
}
3.3 多个 useEffect
这当然是可行的,这正是 useEffect() 实现逻辑分离,从而比生命周期函数更清晰的原理。
(注:多个 useEffect 在渲染时会按照声明的顺序被调用)
useEffect(() => {/*订阅第一个数据源*/} return (()=>{/*清除第一个数据源*/}))
useEffect(() => {/*订阅第二个数据源*/} return (()=>{/*清除第二个数据源*/}))
useEffect(() => {/*添加计时器*/} return (()=>{/*删除计时器*/}))
相比之下,如果使用生命周期函数,就要像下面这样,逻辑全部写在一起:
componentDidMount() {
/*订阅第一个数据源*/
/*订阅第二个数据源*/
/*添加计时器*/
}
componentWillUnmount() {
/*清除第一个数据源*/
/*清除第二个数据源*/
/*删除计时器*/
}
3.4 跳过 useEffect 进行性能优化
useEffect 除了接受一个函数参数外,还能接受第二个参数,为一个列表,只有列表里的值变化的时候 useEffect 才会重新执行。
useEffect(() => {
/*一些处理*/
}, [a]); // 只有当 a 变化的时候,才会重新执行
(注:这个和 shouldComponentUpdate 无关,只是 useEffect 不会被执行,并不会导致不重新渲染。不如说 useEffect 本身也是在重新渲染完毕后才会执行,想阻止也不可能)
(shouldComponentUpdate 的替代为 React.memo)
3.5 仅在挂载时执行的 useEffect
很简单,给 useEffect 的第二个参数传递空数组 [] 即可。
4. Hook 规则
hook 的使用有两条规则:
- hook 只能在函数组件的最外层执行。甚至不能在 if,while,嵌套函数中执行。
- 只能在函数组件中调用 hook。
4.1 为什么不能在 if 等语句中执行
因为 React 是靠 hook 执行顺序判断 state 和 effect 的对应关系的。
如果使用 if,则某些情况下如果这个语句中的 hook 没有执行,则之后的关系就全对应错乱了。
4.2 使用 eslint 插件强制执行规则
插件名字为 eslint-plugin-react-hooks。
- 首先环境配置,CRA 支持直接创建
.eslintrc.json文件并加上以下代码即可扩展默认的 eslint 配置。
{
"extends": "react-app" // 扩展默认的 eslint 配置
}
- 然后安装插件,用 yarn/npm 安装到开发环境。
- 给
eslintrc.json加上以下代码:
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
"react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
}
}
5. 自定义 hook
对应 class 组件中的 HOC 和 render props,用于复用状态逻辑。
用法看文档。看之前有一件事需要知道就是每次重新渲染的时候函数里的非 hook 代码也都会执行。
可以看到,自定义 hook 非常简单,几乎就是把需要复用的代码用函数包裹起来,然后以 use 开头命名以避免破坏 hook 规则。
6. 其他 hook
hook 提供了很多其他 hook 来替代 class 组件的各种功能,比如 context,ref 等,用法都十分相近。 参考文档。