Last updated
Last updated
看一个例子,我们分解来看到底state hooks做了什么:
useState
是react自带的一个hook函数,它的作用就是用来声明状态变量。useState
这个函数接收的参数是我们的状态初始值(initial state),它返回了一个数组,这个数组的第[0]
项是当前当前的状态值,第[1]
项是可以改变状态值的方法函数。
所以我们做的事情其实就是,声明了一个状态变量count,把它的初始值设为0,同时提供了一个可以更改count的函数setCount。
很简单,因为我们的状态count就是一个单纯的变量而已,我们再也不需要写成{this.state.count}
这样了。
当用户点击按钮时,我们调用setCount函数,这个函数接收的参数是修改过的新状态值。接下来的事情就交给react了,react将会重新渲染我们的Example组件,并且使用的是更新后的新的状态,即count=1
。这里我们要停下来思考一下,Example本质上也是一个普通的函数,为什么它可以记住之前的状态?
首先,useState是可以多次调用的,所以我们完全可以这样写:
其次,useState接收的初始值没有规定一定要是string/number/boolean这种简单数据类型,它完全可以接收对象或者数组作为参数。最后,react也给我们提供了一个useReducer
的hook,如果你更喜欢redux式的状态管理方案的话。
注意:之前我们的this.setState
做的是合并状态后返回一个新状态,而useState
是直接替换老状态后返回新状态。
从ExampleWithManyStates
函数我们可以看到,useState
无论调用多少次,相互之间是独立的。这一点至关重要。为什么这么说呢?
其实我们看hook的“形态”,有点类似之前被官方否定掉的Mixins这种方案,都是提供一种“插拔式的功能注入”的能力。而mixins之所以被否定,是因为Mixins机制是让多个Mixins共享一个对象的数据空间,这样就很难确保不同Mixins依赖的状态不发生冲突。
而现在我们的hook,一方面它是直接用在function当中,而不是class;另一方面每一个hook都是相互独立的,不同组件调用同一个hook也能保证各自状态的独立性。这就是两者的本质区别了。
还是看上面给出的ExampleWithManyStates
例子,我们调用了三次useState
,每次我们传的参数只是一个值(如42,‘banana’),我们根本没有告诉react这些值对应的key是哪个,那react是怎么保证这三个useState找到它对应的state呢?
Tips:React是根据useState
出现的顺序来定的。
我们具体来看一下:
假如我们改一下代码:
这样一来:
强制:React规定我们必须把hooks写在函数的最外层,不能写在ifelse
等条件语句当中,来确保hooks的执行顺序一致。
我们在上一节的例子中增加一个新功能:
我们对比着看一下,如果没有hooks,我们会怎么写?
我们写的有状态组件,通常会产生很多的副作用(side effect),比如发起ajax请求获取数据,添加一些监听的注册和取消注册,手动修改dom等等。我们之前都把这些副作用的函数写在生命周期函数钩子里,比如componentDidMount
,componentDidUpdate
和componentWillUnmount
。而现在的useEffect
就相当与这些声明周期函数钩子的集合体。它以一抵三。
同时,由于前文所说hooks可以反复多次使用,相互独立。所以我们合理的做法是,给每一个副作用一个单独的useEffect
钩子。这样一来,这些副作用不再一股脑堆在生命周期钩子里,代码变得更加清晰。
我们再梳理一遍下面代码的逻辑:
首先,我们声明了一个状态变量count
,将它的初始值设为0。然后我们告诉react,我们的这个组件有一个副作用。我们给useEffect
hook传了一个匿名函数,这个匿名函数就是我们的副作用。
在这个例子里,我们的副作用是调用browser API来修改文档标题。当react要渲染我们的组件时,它会先记住我们用到的副作用。等react更新了DOM之后,它再依次执行我们定义的副作用函数。
这里要注意几点:
第一,react首次渲染和之后的每次渲染都会调用一遍传给useEffect
的函数。而之前我们要用两个声明周期函数来分别表示首次渲染(componentDidMount
),和之后的更新导致的重新渲染(componentDidUpdate
)。
第二,useEffect
中定义的副作用函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的,而之前的componentDidMount
或componentDidUpdate
中的代码则是同步执行的。
这种安排对大多数副作用说都是合理的,但有的情况除外,比如我们有时候需要先根据DOM计算出某个元素的尺寸再重新渲染,这时候我们希望这次重新渲染是同步发生的,也就是说它会在浏览器真的去绘制这个页面前发生。
这种场景很常见,当我们在componentDidMount
里添加了一个注册,我们得马上在componentWillUnmount
中,也就是组件被注销之前清除掉我们添加的注册,否则内存泄漏的问题就出现了。
怎么清除呢?让我们传给useEffect
的副作用函数返回一个新的函数即可。这个新的函数将会在组件下一次重新渲染之后执行。这种模式在一些pubsub模式的实现中很常见。看下面的例子:
这里有一个点需要重视!这种解绑的模式跟componentWillUnmount
不一样。componentWillUnmount
只会在组件被销毁前执行一次而已,而useEffect
里的函数,每次组件渲染后都会执行一遍,包括副作用函数返回的这个清理函数也会重新执行一遍。所以我们一起来看一下下面这个问题。
我们先看以前的模式:
我们在componentDidMount
注册,再在componentWillUnmount清除注册。但假如这时候props.friend.id
变了怎么办?我们不得不再添加一个componentDidUpdate
来处理这种情况:
看到了吗?很繁琐,而我们但useEffect则没这个问题,因为它在每次组件更新后都会重新执行一遍。所以代码的执行顺序是这样的:
按照上一节的思路,每次重新渲染都要执行一遍这些副作用函数,显然是不经济的。怎么跳过一些不必要的计算呢?我们只需要给useEffect传第二个参数即可。用第二个参数来告诉react只有当这个参数的值发生改变时,才执行我们传的副作用函数(第一个参数)。
当我们第二个参数传一个空数组[]时,其实就相当于只在首次渲染的时候执行。也就是componentDidMount
加componentWillUnmount
的模式。不过这种用法可能带来bug,少用。