demo
诸位请看下面这块熟悉的代码
ts
const Component = () => {
const [count,setCount] = useState(0)
const [name,setName] = useState('')
const add = () => {
setCount(count+1)
setCount(count+1)
setCount(val=>val+1)
}
useEffect(()=>{
setName(`第${count}个`)
},[count])
return (
<>
<div>
{count}
</div>
<div>
{name}
</div>
<button onClick={add}>增加</button>
</>
)
}
背景
- 类组件:通过属性维护着自身的状态、函数、ui。通过生命周期函数将状态的变更和逻辑代码结合
- 函数组件:通过传入的属性决定状态和ui
很明显,当我们希望count变化时,只执行与count相关的逻辑(add)。如果是类组件的话,我们需要在componentDidUpdate
生命周期中写入setCount逻辑。而name变化时需要执行的逻辑(updateName)也需要写入componentDidUpdate
,反复在componentDidUpdate
写入逻辑,耦合度的变高会导致可读性、可复用性、可测试性降低。
那怎么办,函数组件又没有状态管理能力。
所以React Hooks来了,它的特点如下:
- 在函数式组件的基础上引入了state,并让state的每一个属性自治,而不是放入一个state对象中管理
- 引入useEffect,使得state的更新与更新逻辑可一对一的关联,锁定依赖的影响范围
- 引入useMemo、useCallBack,使得组件的性能优化更方便的实现了(各位可以想想类组件想实现状态的缓存是不是会很麻烦)
- 去处了没必要的模板代码,比如类函数的构造函数、生命周期函数
- 依赖的影响范围锁定之后,逻辑不会在生命周期函数中冗合,代码的复用性、可测试性提高了。
请看文章一开始的demo,count控制着一个ui,而count的更新逻辑add能很简单的和count的变化绑定起来,而且代码层级扁平化,没有生命周期函数的嵌套,没有state嵌套count,易于阅读,易于测试,易于控制范围。
原理
react hooks中引入了很多的hook,useState
、useEffect
、useMome
、useCallback
、useRef
等
他们都有其各自的作用,其底层原理各不相同,这篇文章会以useState
、useEffect
抛砖引玉,各位甚至可以自己给react-hooks添加属于自定义的hook。
前置知识补充:
- 函数组件在react底层以
fiberNode
形式存在,它有以下属性
tag
:指定组件是类组件/函数组件/原生dom标签key
:唯一键children
:子代组件集合sibling
:本组件的下一个兄弟组件,主要用于将树的层级结构打平,不然树的层级过深会导致函数调用栈会爆满memorizedState
:存储组件的状态,比如函数组件的hook链表updateQueue
:组件状态改变后的回调函数,比如useEffect、useMemo的函数
- 函数组件在状态变化后会重新执行自身函数
以文章开头的demo为例,初次执行这个函数组件时,即mount阶段,执行到第一个useState
、第二个useState
后,这两个hook会以链表的形式存储在fiberNode的memorizedState
中,如下图所示。
执行到useEffect
后,这个hook会存入hook链表中,然后将自身的effect放入fiberNode的updateQueue
中,如下图
当我们点击增加按钮
时,会执行setCount(count+1)
、setCount(count+1)
、setCount(val=>val+1)
,这三个函数会以循环链表的形式存储在count的useState的pending
中
每次执行useState
返回的reducer
时,即setCount
、setName
等,会触发一个scheduleWork
函数,该函数最终会去触发组件的重新渲染。
这里会有一个问题:短时间内触发多次setCount就会触发多次scheduleWork
,那是不是就会短时间内触发多次重渲染呢?
结论:react会将多个scheduleWork
合并做批量更新操作,并且会根据很多因素(比如任务队列)来调度这些更新任务,各位对这里有兴趣的,可以自行查看源码。
触发更新后,函数式组件会重新执行,进入update阶段,当再次执行到第一个useState
、第二个useState
后,依旧会将他们放入hook链表中。
当再次执行到useEffect
后,会执行上一次的useEffect的effect中的return函数
,然后比较更新前后的依赖是否更新了,如果依赖更新了,则这本次的effect打上更新flag,存入fiberNode的updateQueue中;否则不打上更新flag,再存入updateQueue中。
然后更新ui
然后执行firberNode的updateQueue中有更新flag的effect。