本文是 React 新特性系列的第二篇,第一篇请点击这里:
React 新特性讲解及实例
什么是 Hooks
Hook 是 React 16.8 的新增特性。它可以让你在不编写 类组件 的情况下使用 state
以及其他的 React
特性。
类组件的不足
状态逻辑复用难
缺少复用机制
渲染属性和高阶组件导致层级冗余
趋向复杂难以维护
生命周期函数混杂不相干逻辑
相干逻辑分散在不同生命周期
this 指向困扰
内联函数过度创建新句柄
类成员函数不能保证
this
Hooks 优势
优化类组件的三大问题
函数组件无 this 问题
自定义 Hook 方便复用状态逻辑
副作用的关注点分离
使用 State Hook
import React, {Component} from 'react'
class App extends Component {
state = {
count: 0
};
render() {
const {count} = this.state;
return (
<button type="button"
onClick={() => {
this.setState({
count: count + 1
})
}}
>Click({count})button>
)
}
}
export default App;
以上代码很好理解,点击按钮让 count
值加 1
。
接下来我们使用 useState
来实现上述功能。
import React, {useState} from 'react'
function App () {
const [count, setCount] = useState(0)
return (
<button type="button"
onClick={() => {setCount(count + 1) }}
>Click({count})button>
)
}
在这里, useState
就是一个 Hook。通过在函数组件里调用它来给组件添加一些内部 state
,React 会在重复渲染时保留这个 state
。
useState
会返回一对值:当前状态和一个让你更新它的函数。你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class
组件的 this.setState
,但是它不会把新的 state
和旧的 state
进行合并。 useState
唯一的参数就是初始 state
。
useState
让代码看起来简洁了,但是我们可能会对组件中,直接调用 useState
返回的状态会有些懵。既然 userState
没有传入任何的环境参数,它怎么知道要返回的的是 count
的呢,而且还是这个组件的 count
不是其它组件的 count
。
初浅的理解: useState
确实不知道我们要返回的 count
,但其实也不需要知道,它只要返回一个变量就行了。数组解构的语法让我们在调用 useState
时可以给 state
变量取不同的名字。
useState
怎么知道要返回当前组件的 state
?
因为 JavaScript 是单线程的。在 useState
被调用时,它只能在唯一一个组件的上下文中。
有人可能会问,如果组件内有多个 usreState
,那 useState
怎么知道哪一次调用返回哪一个 state
呢?
这个就是按照第一次运行的次序来顺序来返回的。
接着上面的例子我们在声明一个 useState
:
...
const [count, setScount] = useState(0)
const [name, setName] = useState('小智')
...
然后我们就可以断定,以后 APP
组件每次渲染的时候, useState
第一次调用一定是返回 count
,第二次调用一定是返回 name
。不信的话来做个实验:
let id = 0
function App () {
let name,setName;
let count,setCount;
id += 1;
if (id & 1) {
// 奇数
[count, setCount] = useState(0)
[name, setName] = useState('小智')
} else {
// 偶数
[name, setName] = useState('小智')
[count, setCount] = useState(0)
}
return (
<button type="button"
onClick={() => {setCount(count + 1) }}
>
Click({count}), name ({name})
button>
)
}
首先在外部声明一个 id,当 id为奇数和偶数的时候分别让 useState 调用方式相反,运行会看到有趣的现象。
当前版本如果写的顺序不一致就会报错。
会发现 count
和 name
的取值串了。我们希望给 count 加 1
,现在却给 name 加了 1
,说明 setCount
函数也串成了 setName
函数。
为了防止我们使用 useState 不当,React 提供了一个 ESlint 插件帮助我们检查。
eslint-plugin-react-hooks
优化点
通过上述我们知道 useState
有个默认值,因为是默认值,所以在不同的渲染周期去传入不同的值是没有意义的,只有第一次传入的才有效。如下所示:
...
const defaultCount = props.defaultCount || 0
const [count, setCount] = useState(defaultCount)
...
state
的默认值是基于 props
,在 APP 组件每次渲染的时候 constdefaultCount=props.defaultCount||0
都会运行一次,如果它复杂度比较高的话,那么浪费的资料肯定是可观的。
useState
支持传入函数,来延迟初始化:
const [count, setCount] = useState(() => {
return props.defaultCount || 0
})
使用 Effect Hook
Effect Hook 可以让你在函数组件中执行副作用操作。数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是"副作用"这个名字,应该都在组件中使用过它们。
副作用的时机
Mount 之后 对应
componentDidMount
Update 之后 对应
componentDidUpdate
Unmount 之前 对应
componentWillUnmount
现在使用 useEffect
就可以覆盖上述的情况。
为什么一个 useEffect
就能涵盖 Mount,Update,Unmount
等场景呢。
useEffect 标准上是在组件每次渲染之后调用,并且会根据自定义状态来决定是否调用还是不调用。
第一次调用就相当于 componentDidMount
,后面的调用相当于 componentDidUpdate
。 useEffect
还可以返回另一个回调函数,这个函数的执行时机很重要。作用是清除上一次副作用遗留下来的状态。
比如一个组件在第三次,第五次,第七次渲染后执行了 useEffect
逻辑,那么回调函数就会在第四次,第六次和第八次渲染之前执行。严格来讲,是在前一次的渲染视图清除之前。如果 useEffect
是在第一次调用的,那么它返回的回调函数就只会在组件卸载之前调用了,也就是 componentWillUnmount
。
如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做
componentDidMount
,componentDidUpdate
和componentWillUnmount
这三个函数的组合。
举粟说明一下:
class App extends Component {
state = {
count: 0,
size: {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
}
};
onResize = () => {
this.setState({
size: {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
}
})
}
componentDidMount () {
document.title = this.state.count;
window.addEventListener('resize', this.onResize, false)
}
componentWillMount () {
window.removeEventListener('resize', this.onResize, false)
}
componentDidUpdate () {
document.title = this.state.count;
}
render() {
const {count, size} = this.state;
return (
<button type="button"
onClick={() => {this.setState({count: count + 1})}}
>
Click({count})
size: {size.width}x{size.height}
button>
)
}
}
上面主要做的就是网页 title
显示 count
值,并监听网页大小的变化。这里用到了 componentDidMount
, componentDidUpdate
等副作用,因为第一次挂载我们需要把初始值给 title, 当 count
变化时,把变化后的值给它 title
,这样 title
才能实时的更新。
注意,我们需要在两个生命周期函数中编写重复的代码。
这边我们容易出错的地方就是在组件结束之后要记住销毁事件的注册,不然会导致资源的泄漏。现在我们把 App 组件的副作用用 useEffect
实现。
function App (props) {
const [count, setCount] = useState(0);
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
const onResize = () => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
}
)
}
useEffect(() => {
document.title = count;
})
useEffect(() => {
window.addEventListener('resize', onResize, false);
return () => {
window.removeEventListener('resize', onResize, false)
}
}, [])
return (
<button type="button"
onClick={() => {setCount(count + 1) }}
>
Click({count})
size: {size.width}x{size.height}
button>
)
}
对于上述代码的第一个 useEffect
,相比类组件,Hooks 不在关心是 mount 还是 update。用 useEffect
统一在渲染后调用,就完整追踪了 count
的值。
对于第二个 useEffect
,我们可以通过返回一个回调函数来注销事件的注册。回调函数在视图被销毁之前触发,销毁的原因有两种:重新渲染和组件卸载。
这边有个问题,既然 useEffect
每次渲染后都执行,那我们每次都要绑定和解绑事件吗?当然是完全不需要,只要使用 useEffect
第二个参数,并传入一个空数组即可。第二个参数是一个可选的数组参数,只有数组的每一项都不变的情况下, useEffect
才不会执行。第一次渲染之后,useEffect 肯定会执行。由于我们传入的空数组,空数组与空数组是相同的,因此 useEffect
只会在第一次执行一次。
这也说明我们把 resize
相关的逻辑放在一直写,不在像类组件那样分散在两个不同的生命周期内。同时我们处理 title 的逻辑与 resize 的逻辑分别在两个 useEffect 内处理,实现关注点分离。
我们在定义一个 useEffect,来看看通过不同参数,第二个参数的不同作用。
...
useEffect(() => {
console.log('count:', count)
}, [count])
...
第二个参数我们传入 [count]
, 表示只有 count 的变化时,我才打印 count
值, resize
变化不会打印。
运行效果如下:
第二个参数的三种形态, undefined
,空数组及非空数组,我们都经历过了,但是咱们没有看到过回调函数的执行。
现在有一种场景就是在组件中访问 Dom 元素,在 Dom元素上绑定事件,在上述的代码中添加以下代码:
...
const onClick = () => {
console.log('click');
}
useEffect(() => {
document.querySelector('#size').addEventListener('click', onClick, false);
},[])
return (
...
<span id="size">size: {size.width}x{size.height}span>
div>
)
新增一个 DOM 元素,在新的
useEffect
中监听 span
元素的点击事件。
运行效果:
假如我们 span 元素可以被销毁重建,我们看看会发生什么情况,改造一下代码:
return (
...
button>
{
count%2
? <span id="size">我是spanspan>
: <p id='size'>我是pp>
}
div>
运行效果:
可以看出一旦 dom 元素被替换,我们绑定的事件就失效了,所以咱们始终要追踪这个dom 元素的最新状态。
使用
useEffect
,最合适的方式就是使用回调函数来处理了,同时要保证每次渲染后都要重新运行,所以不能给第二次参数设置 []
,改造如下:
useEffect(() => {
document.querySelector('#size').addEventListener('click', onClick, false);
return () => {
document.querySelector('#size').removeEventListener('click', onClick, false);
}
})
运行结果:
参考
[React 官方文档][9]
《React劲爆新特性Hooks 重构去哪儿网》
交流
交流
我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,即可看到福利,你懂的。