前言
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
(React-native 0.59,Taro 1.3开始已支持hooks)
类组件的不足
组件之间复用状态逻辑很难
- 缺少复用机制
- 渲染属性和高阶组件导致层级冗余
- 趋向复杂难以维护
生命周期函数混杂不相干逻辑
- 相干逻辑分散在不同生命周期
- this 指向困扰
绑定this的几种方式
- 在构造函数中绑定this(构造函数每一次渲染的时候只会执行一遍)
- 在
render()
函数中绑定this
(在每次render()
的时候都会重新执行一遍函数) - 使用箭头函数 (每一次render()的时候,都会生成一个新的箭头函数,即使两个箭头函数的内容是一样的)
Hoosk优点
解决类组件的三个问题
- 解决this指向问题
- 加强逻辑复用性
- 不同生命周期导致的复杂组件难以理解
使用State Hook
从一个例子开始
一个简单的有状态组件,点击后次数加一:
import React, {Component} from 'react'
class App extends Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
render() {
return (
<div>
<p>Clicked {this.state.count} times</p>
<button onClick={
() => this.setState({ count: this.state.count +1})
}>
Click me
</button>
</div>
);
}
}
经过Hooks改造后:
import {useState} from 'react'
function App () {
const [count,setCount] = useState(0)
return (
<div>
<p>Clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
可以看到,改造后的代码量简洁了很多,并且相比无状态组件多了个自己的state(count),并且可以更新自己的状态(setCount)。其中useState
就是Hook中的一个,就是通过他使得函数内部拥有自己的state
。这里有点要说明,正常来说不考虑闭包的情况下,函数中定义的变量在执行完后就会被销毁,但是上面的App却不是,每次都是拿的上一次执行完的状态值作为初始值,说明这个变量被React保留了,至于其中机制,就需要深入去理解了。(通过memorizedState去维护- 2019-12-25补充)
useState
使用方式很简单,传入一个初始值state
,返回一个数组,返回的数组包含当前state和一个更新state的函数。相比类组件的this.setState
,这里有了setCount
和count
变量可以直接使用,所以说为什么Hook解决了this问题
组件有多个状态怎么办
useState
是可以使用多次的,所以多个状态只需调用多次useState()
,就像下面这样:
function manyState() {
const [num, setNum] = useState(0)
const [str, setStr] = useState('diego')
const [bool, setBool] = useState(true)
....
}
其次,useState接收的初始值没有规定一定要是string/number/boolean这种简单数据类型,它完全可以接收对象或者数组作为参数。另外在类组件中,this.setState()
是合并状态后返回,而useState
是直接替换老状态返回新状态。
那么,如果组件内有多个usreState
,那useState
怎么知道哪一次调用返回哪一个state
呢?
答案是,react是根据useState出现的顺序来定的(通过链表来实现)。如下:
function human() {
//第一次渲染
//将money初始化为0
const [money, setMoney] = useState(0);
//将sex初始化为man
const [sex,setSex] = useState('man');
//...
const [info, setInfo] = useState([{ house: flse }]);
//第二次渲染
//读取状态变量money的值(这时候传的参数0被忽略)
const [money, setMoney] = useState(0);
//读取状态变量sex的值(这时候传的参数man被忽略)
const [sex,setSex] = useState('man');
const [info, setInfo] = useState([{ house: flse }]); //...
}
修改以上代码
let showSex = true;
function human() {
const [money, setMoney] = useState(0);
if(showSex) {
const [sex,setSex] = useState('man');
showSex = false;
}
const [info, setInfo] = useState([{ house: false }]);
}
渲染结果如下:
//第一次渲染
//将money初始化为0
const [money, setMoney] = useState(0);
//将sex初始化为man
const [sex,setSex] = useState('man');
//...
const [info, setInfo] = useState([{ house: flse }]);
//第二次渲染
//读取状态变量money的值(这时候传的参数0被忽略)
const [money, setMoney] = useState(0);
// const [sex,setSex] = useState('man');
// 读取到状态变量Sex的值,报错
const [info, setInfo] = useState([{ house: flse }]);
react规定我们必须把hooks写在函数的最外层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致。并且为了防止我们使用useState不当,React提供了一个ESlint 插件帮助我们检查。
优化点
useState
有个默认值,因为是默认值,所以在不同的渲染周期去传入不同的值是没有意义的,如下面代码,只有第一次传入的才有效:
...
const defaultValue = props.value || 0
const [value, setValue] = useState(defaultValue)
...
state
的默认值是基于props
,在组件每次渲染的时候const defaultValue = props.value || 0
都会运行一次,如果它复杂度比较高的话,那么浪费的性能肯定是较大的。
useState()
支持传入函数,来延迟初始化,就像setState
回调那样:
...
const [value, setValue] = useState(() => {
return props.value || 0
})
...
使用Effect Hook
使用过类组件的都知道,它提供了一套生命周期用于在不同情况下进行对应的操作,比如发送网络请求(componentDidMount),更新数据(componentDidUpdate),组件销毁(componentWillUnmount)等,这些在react中被统称作 ‘副作用’,同时也会遇到类似以下的情况,在最开始的例子中加入一个功能,每次点击后更新页面的title:
...
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
...
可以看到需要在两个生命周期函数中编写重复的代码,即使提取了公共方法,也需要在两地地方去调用,这个时候怎么办呢?于是Effect Hook
应用而生,作为componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的集合,可以使用它在函数组件中解决副作用操作。同样以上面那个例子,经过改造后的代码如下
...
useEffect(() => {
document.title = `You clicked ${count} times`;
});
...
-
useEffect做了什么?
useEffect
标准上是在组件每次渲染之后调用,并且会根据自定义状态来决定是否调用还是不调用。
第一次调用就相当于componentDidMount
,后面的调用相当于componentDidUpdate
。 -
useEffect怎么解绑一些副作用
useEffect
还可以返回另一个回调函数,这个函数的执行时机很重要。作用是清除上一次副作用遗留下来的状态。 -
useEffect使用时机
按照上面的说法,每次渲染都要执行这些副作用函数,如果只是这样那明显是不合理啊?好在
useEffect
提供了第二个参数,第二个参数是一个可选的数组参数,只有数组的每一项都不变的情况下,useEffect
才不会执行。第一次渲染之后,useEffect
肯定会执行。如果我们传入的空数组,空数组与空数组是相同的,这样一来useEffect
只会在第一次执行一次。
按照以上思路再次对例子进行改造:
...
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); //只有当count的值发生变化时,才会重新执行`document.title`这一句
...
useEffect实际应用
function initVal() {
const arr = []
for (let i = 0; i < 10; i++) {
arr.push({
id: i
})
}
return arr;
}
const Test = (props) => {
const [list, setList] = useState(() => {
return props.list || initVal()
})
const doScroll = () => {
// 滚动高度
let scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop
// 文档高度
let scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight
// 可视高度
let clientHeight = document.documentElement.clientHeight || document.body.clientHeight
if (scrollTop + clientHeight > scrollHeight - 50) {
getList()
}
}
const getList = () => {
const newList = [].concat(list).concat(initVal())
setList(newList)
}
// 相当于componentDidMount 只执行一次
useEffect(() => {
console.log(222)
getList()
},[])
// 每次list更新都执行
useEffect(() => {
console.log(111)
window.addEventListener('scroll', doScroll, false)
return () => {
console.log(333)
window.removeEventListener('scroll', doScroll, false)
}
}, [list])
return (
<div>
{
list.map( (item, index) => {
return (
<div style={{height: '200px','textAlign':'center','borderBottom': '1px solid #000','fontSize': '30px'}} key={index}>{item.id}</div>
)
})
}
</div>
)
}