前言
自react16.8发布了正式版hook用法以来,我们公司组件的写法逐渐由class向函数式组件+hook的方向转移,虽然用了这么久的hook,但是用得多的基本就useState
、useEffect
和useMemo
,其他的官方hook因为使用场景不明导致基本没用过,所以这两天特地去了解了一下其他hook的使用场景以及useState
的原理,然后用这篇文章记录一下。
useState的使用及其原理
在hook版本出来之前,react函数组件无法拥有自身内部的状态,而useState
赋予了函数组件拥有内部状态的能力,并且它的使用非常简单。
-
useState用法
useState
是一个函数,它接收一个初始值,并返回一个数组,该数组的第一位是一个state,第二位则是改变这个的state的函数,比如下面这个计数器的例子,当我点击按钮+的时候,数字就+1:
上例中的n就是这个函数组件的内部状态了。 -
useState原理
在上面的例子中,我们每次点击按钮+的时候,数字都会增加1,也就是说App这个函数会被重新执行一次:
既然App会被重新执行,那么useState(0)
也会被重新执行一次,但是为什么n的值不会被重置为0呢?
原因是,第二次useState
执行后返回的n并非之前的n,setN
改变的并不是之前返回出来的那个n,setN
改变的数值存储于其它地方而非n,之后useState
通过闭包的形式将这个新的数值返回了出来,并且执行dom的更新,我们可以在下面的实现一个useState
看得更清楚。 -
实现一个useState
-
首先通过上面的原理的解析,
setN
改变的并非n,而是另一个变量,所以我们创建这个变量_state
,并创建myUseState
函数:
-
之后
myUseState
接收一个初始值,并返回一个数组,注意这个数组的第一项返回的并不是接收到的初始值而是第一步_state
,而第二项则是改变_state
的函数:
另外需要注意的是,在第一次执行myUseState
的时候需要将初始值赋值给_state
,而第二次执行的时候则是将之前的_state
赋值回_state
:
-
接着
useState
在更新state的时候会重新渲染Dom,所以我们在setState
函数中执行重新渲染的步骤(这里为了方便简化了更新步骤):
-
这时候我们自己的
useState
就实现完成分了,用来测试一下:
结果可见是成功的:
但是此时我们的myUseState
存在一个严重的bug,如果一个组件内存在多个state,而_state
却只有一个,就会导致多个state都共用了一个状态,比如下面的组件:
-
-
修复myUseState的bug
-
针对上面所说的bug,在组件拥有多个state的情况下,
useState
的执行存在由上到下的顺序,那么我们就可以将_state
改造为一个数组,用于存储多个state,另外还需要新建一个变量index
用于表明state在_state
中的顺序:
-
之后在
myUseState
中要做的第一步就是将接收到的state放入到_state
中(注意这里和之前一样,第一次执行放入的是初始state,从第二次开始变成_state
中对应的state):
-
第三步我们在
myUseState
中创建一个能够修改_state
中对应数据的函数setState
并将其返回出来:
-
之后需要考虑,
setState
执行的时候会重新渲染组件,所以在这一步中需要重置index
:
-
另外,为了保证每个
_state
中的state
的顺序是一致的,所以在myUseState
中将state放入到_state
之后,将index + 1
,这样我们就修复了之前多个state冲突的问题了:
-
测试结果和代码总览:
-
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
const _state = []
let index = 0
const myUseState = initialValue => {
const currentIndex = index
_state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex]
index = index + 1
const setState = newValue => {
_state[currentIndex] = newValue
index = 0
ReactDOM.render(<App />, document.getElementById('app'))
}
return [_state[currentIndex], setState]
}
const App = () => {
const [n, setN] = myUseState(0)
const [m, setM] = myUseState(0)
const clickN = () => {
setN(n + 1)
}
const clickM = () => {
setM(m + 1)
}
return (
<div>
<div>{n}</div>
<button onClick={clickN}>+</button>
<div>{m}</div>
<button onClick={clickM}>+</button>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('app'))
- useState的一些其他知识
-
useState
不能在条件语句后使用的原因: 原因根据上面自己实现的myUseState
中就能看出来了,如果放在条件语句后使用,那么就有可能打破_state
存放state
的顺序导致state错乱。 - 看下面代码,虽然用了两次
setN
,但实际上点击按钮+后实际上数字只会加一:
我们可以将clickN
中的代码改成如下,使其变成每次都能+2:
-
useEffect和useLayoutEffect的使用及其异同
-
useEffect
useEffect
接收两个参数,第一个参数是函数,用于当组件内的state产生变化之后执行,而第二个参数(非必传)是一个数组,接收依赖的state,比如下面的例子,当n变化的时候将会打印出n的数值:
另外useEffetc
接收的函数参数可以返回一个函数,这个函数将在该组件注销时执行,类似于class组件的componentWillUnmount
。
例如下面的组件,在组件挂载后会设置一个定时器,每一秒钟打印一个1出来,当该组件被注销后,这个定时也会被注销:
另外还需要注意,usweEffect
接收的函数是在组件渲染完毕之后才执行的。 -
useLayoutEffect
useLayoutEffect
用的非常少,这是一个有点像vue的v-cloak
的功能,比如下面的代码,当组件挂载之后,把div里面的文字从value: 0
改成value: 1000
:
我们看到的效果确实也是这样的:
但是当你刷新多几次的时候,仔细观察就会发现,每次加载进来页面都会看到value: 0
闪烁一下然后变成value: 1000
,这是因为useEffect
接收的函数是在组件被渲染之后才会执行的。
这时候要解决这个问题,就需要将useEffect
改成useLayoutEffect
了,就不会存在这个闪烁的问题,而是直接显示value: 1000
:
原因在于,useLayoutEffect
接收到的函数参数在组件渲染之前就会被执行,也就是说useEffect
和useLayoutEffect
功能其实是类似的,但是执行的时机不同,我们可以从下面的执行顺序看出来:
打印的顺序确实是1 2 3 4
:
注意:
虽然说useLayoutEffect
能够在useEffect
之前就执行,但是在不改变网页Dom文字样式的情况下,还是推荐使用useEffect
的,在需要改变网页Dom文字样式的情况下再使用useLayoutEffect
useReducer以及useContext
-
useReducer
useReducer
的使用和redux
的使用有些类似,useReducer
接收两个参数,第一个是reducer
(和redux中的一模一样),第二个参数是初始state,之后他会返回一个数组,数组第一项是state,第二项是改变state的函数dispatch,比如下面的例子:
测试结果:
-
useContext
useContext
需要和createContext
结合起来使用,实际上他们所要解决的问题和redux、mobx是类似的,都是夸组件间的数据传递,比如下面的例子,存在App组件,一个父亲组件,一个儿子组件,我们就通过创建一个Context,并用这个Context将App组件包裹起来,将App组件内的state传入到Context,使得父亲组件和儿子组件都能够通过useContext
拿到App组件的state:
useReducer和useContext结合搭建状态管理系统
使用useContext
可以在任意被对应Context包裹的组件中拿到传入的数据,将其和useReducer结合起来,
就可以创建一个组件的状态管理系统,如何搭建可以参考我的这篇文章从零搭建项目(5) --- 前端: 搭建路由和状态管理
React.memo、useMemo和useCallback
这三个Api通常都在优化组件的时候使用,并且他们使用的都是记忆化函数的原理,关于记忆化函数可以参考我之前写的这篇文章: 再谈js中的函数
-
React.memo
memo
的功能其实之前class组件的pureComponent差不多,但是这个memo
是用在函数式组件上的。
首先我们来看下面的例子,Child组件引用了App组件的状态m
,状态n
和Child组件并无关系:
但实际上我点击按钮并执行setN
的时候,Child组件也被更新了:
原因是Child组件被App组件所包裹,而执行setN
的时候,App组件被重新渲染了,那么在其之中的Child组件自然也就被重新渲染了。
所以这时候我们就需要用到memo
来优化一下,使得我在执行setN
的时候,Child组件不会跟着一起被渲染。
memo
的使用也非常简单,直接用它包裹需要被优化的组件即可,在本例中就是Child组件,所以代码可以修改为如下:
这时候我们执行setN
的时候就不会使得Child组件跟着重新渲染了,只有执行setM
的时候Child才会重新渲染:
-
useCallback
在上面使用memo
的例子中,存在一个问题,当Child接收的props中存在函数的时候,之前使用memo做的优化就无效了,比如下面的代码:
结果:
原因和之前一样,由于App组件的重新渲染,所以const test = () => {}
这段代码也被重新执行了,而test是一个函数,函数是引用类型,所以传入到Child中的test也和之前的test函数不一样,导致Child组件重新渲染。
这时候我们就可以使用useCallback
对其进行优化了。
useCallback
接收两个参数,首参是一个函数,在本例子中就是test函数,第二个参数是一个数组,这个数组接收的是改变这个函数引用的依赖,比如下面例子,m的值被改变的时候,test函数的引用才会被改变,Child组件才会被重新渲染:
优化结果:
- useMemo类似于vue中computed的功能,他接收两个参数,第一个参数是一个函数并通过计算得出一个state,第二个参数是计算这个state所需要的依赖,比如下面的例子,Child组件接收多一个props:
num
,这个num
是n与m相加得出的:
结果:
useRef和forwardRef
-
useRef
useRef
接收一个参数作为初始值,返回一个可变的 ref 对象,这个 ref 对象含有.current
属性,该属性可以在整个组件色生命周期内不变。
通常useRef
被用作获取某个Dom,比如下面的例子:
-
forwardRef
forwardRef
这个函数用的场景相对较少,它主要用于在父组件获取子组件的Dom作为自己的ref的时候使用,比如下面的例子:
但实际上这样做是有问题的,会报错,并且buttonRef
也没有获取到:
这时候我们就可以使用forwardRef
对Button组件进行包裹,forwardRef
会为Button组件注入一个新的参数ref:
这时候父组件就可以获取得到这个子组件的Dom了: