React 源码分析 (三):useState,useReducer
热身准备
在正式讲useState
,我们先热热身,了解下必备知识。
为什么会有 hooks
大家都知道hooks
是在函数组件的产物。之前class
组件为什么没有出现hooks
这种东西呢?
答案很简单,不需要。
因为在class
组件中,在运行时,只会生成一个实例,而在这个实例中会保存组件的state
等信息。在后续的更新操作中,也只是调用其中的render
方法,实例中的信息不会丢失。而在函数组件中,每次渲染,更新都会去执行这个函数组件,所以在函数组件中是没办法保存state
等信息的。为了保存state
等信息,于是有了hooks
,用来记录函数组件的状态,执行副作用。
hooks 执行时机
上面提到,在函数组件中,每次渲染,更新都会去执行这个函数组件。所以我们在函数组件内部声明的hooks
也会在每次执行函数组件时执行。
在这个时候,可能有的同学听了我上面的说法(**hooks
用来记录函数组件的状态,执行副作用**),又有疑惑了,既然每次函数组件执行都会执行hooks
方法,那hooks
是怎么记录函数组件的状态的呢?
答案是,记录在函数组件对应的fiber
节点中。
两套 hooks
在我们刚开始学习使用hooks
时,可能会有疑惑, 为什么hooks
要在函数组件的顶部声明,而不能在条件语句或内部函数中声明?
答案是,React
维护了两套hooks
,一套用来在项目初始化mount
时,初始化hooks
。而在后续的更新操作中会基于初始化的hooks
执行更新操作。如果我们在条件语句或函数中声明hooks
,有可能在项目初始化时不会声明,这样就会导致在后面的更新操作中出问题。
hooks 存储
提前讲一下 hooks 存储方式,避免看晕了~~~
每个初始化的hook
都会创建一个hook
结构,多个hook
是通过声明顺序用链表的结构相关联,最终这个链表会存放在fiber.memoizedState
中:
而在每个hook.queue
中存放的么个update
也是一个链表结构存储的,千万不要和hook
的链表搞混了。
接下来,让我们带着下面几个问题看文章:
为什么
setState
后不能马上拿到最新的state
的值?多个
setState
是如何合并的?setState
到底是同步还是异步的?为什么
setState
的值相同时,函数组件不更新?
假如我们有下面这样一段代码:
初始化 mount
useState
我们先来看下useState()
函数:
上面的dispatcher
就会涉及到开始提到的两套hooks
的变换使用,initialState
是我们传入useState
的参数,可以是基础数据类型,也可以是函数,我们主要看dispatcher.useState(initialState)
方法,因为我们这里是初始化,它会调用mountState
方法:相关参考视频讲解:进入学习
上面的代码还是比较简单,主要就是根据useState()
的入参生成一个queue
并保存在hook
中,然后将入参和绑定了两个参数的dispatchAction
作为返回值暴露到函数组件中去使用。
这两个返回值,第一个hook.memoizedState
比较好理解,就是初始值,第二个dispatch
,也就是dispatchAction.bind(null, currentlyRenderingFiber$1, queue)
这是个什么东西呢?我们知道使用useState()
方法会返回两个值state, setState
,这个setState
就对应上面的dispatchAction
,这个函数是怎么做到帮我们设置state
的值的呢?
我们先保留这个疑问,往下看,在后面会慢慢揭晓答案。
接下来我们主要看看mountWorkInProgressHook
都做了些什么。
mountWorkInProgressHook
从上面的currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
这一行代码,我们可以发现,hook 是存放在对应fiber.memoizedState
上的。
workInProgressHook = workInProgressHook.next = hook;
,从这一行代码,我们能知道,如果是有多个hook
,他们是以链表的形式进行的存放。
不仅仅是useState()
这个hook
会在初始化时走mountWorkInProgressHook
方法,其他的hook
,例如:useEffect, useRef, useCallback
等在初始化时都是调用的这个方法。
到这里我们能搞明白两件事:
hooks
的状态数据是存放在对应的函数组件的fiber.memoizedState
;一个函数组件上如果有多个
hook
,他们会通过声明的顺序以链表的结构存储;
到这里,我们的useState()
已经完成了它初始化时的所有工作了,简单概括下,useState()
在初始化时会将我们传入的初始值以hook
的结构存放到对应的fiber.memoizedState
,以数组形式返回[state, dispatchAction]
。
更新 update
当我们以某种形式触发setState()
时,React
也会根据setState()
的值来决定如何更新视图。
在上面讲到,useState
在初始化时会返回[state, dispatchAction]
,那我们调用setState()
方法,实际上就是调用dispatchAction
,而且这个函数在初始化时还通过bind
绑定了两个参数, 一个是useState
初始化时函数组件对应的fiber
,另一个是hook
结构的queue
。
来看下我精简后的dispatchAction
(去除了和setState
无关的代码)
上面的代码已经是我尽力精简的结果了。。。代码上有注释,各位看官凑合看下。
不愿细看的我来总结下dispatchAction
做的事情:
创建一个
update
并加入到fiber.hook.queue
链表中,并且链表指针指向这个update
;判断当前是否是渲染阶段决定要不要马上调度更新;
判断这次的操作和上次的操作是否相同, 如果相同则不进行调度更新;
满足上述条件则将带有
update
的fiber
进行调度更新;
到这里我们又搞明白了一个问题:
为什么setState
的值相同时,函数组件不更新?
updateState
我们这里不详细讲解调度更新的过程, 后面文章安排, 这里我们只需要知道,在接下来更新过程中,会再次执行我们的函数组件,这时又会调用useState
方法了。前面讲过,React 维护了两套hooks
,一套用于初始化, 一套用于更新。 这个在调度更新时就已经完成了切换。所以我们这次调用useState
方法会和之前初始化有所不同。
这次我们进入useState
,会看到其实是调用的updateState
方法
看到这几行代码,看官们应该就明白为什么网上有人说useState
和useReducer
相似。原来在useState
的更新中调用的就是updateReducer
啊。
updateReducer
本来很长,想让各位看官忍一忍。于心不忍,忍痛减了很多
上面的更新中,会循环遍历update
进行一个合并操作,只取最后一个setState
的值,这时候可能有人会问那直接取最后一个setState
的值不是更方便吗?
这样做是不行的,因为setState
入参可以是基础类型也可以是函数, 如果传入的是函数,它会依赖上一个setState
的值来完成更新操作,下面的代码就是上面的循环中的reducer
到这里我们搞明白了一个问题,多个setState
是如何合并的?
updateWorkInProgressHook
下面是伪代码,我把很多的逻辑判断给删除了,免了太长又让各位看官难受,原来的代码里会判断当前的hook
是不是第一个调度更新的hook
,我这里为了简单就按第一个来解析
从上面代码能看出来,updateWorkInProgressHook
抛去那些判断, 其实做的事情也很简单,就是基于fiber.memoizedState
创建一个新的hook
结构覆盖之前的hook
。前面dispatchAction
讲到会把update
加入到hook.queue
中,在这里的newHook.queue
上就有这个update
。
总结
总结下useState
初始化和setState
更新:
useState
会在第一次执行函数组件时进行初始化,返回[state, dispatchAction]
。当我们通过
setState
也就是dispatchAction
进行调度更新时,会创建一个update
加入到hook.queue
中。当更新过程中再次执行函数组件,也会调用
useState
方法,此时的useState
内部会使用更新时的hooks
。通过
updateWorkInProgressHook
获取到dispatchAction
创建的update
。在
updateReducer
通过遍历update
链表完成setState
合并。返回
update
后的[newState, dispatchAction]
.
还有两个问题
为什么
setState
后不能马上拿到最新的state
的值?React
其实可以这么做,为什么没有这么做,因为每个setState
都会触发更新,React
出于性能考虑,会做一个合并操作。所以setState
只是触发了dispatchAction
生成了一个update
的动作,新的state
会存储在update
中,等到下一次render
, 触发这个useState
所在的函数组件执行,才会赋值新的state
。setState
到底是同步还是异步的?同步的,假如我们有这样一段代码:
你会惊奇的发现页面还没有更新count
,但是控制台已经打印了after setCount
。
之所以表现上像是异步,是因为内部使用了try{...}finally{...}
。当调用setState
触发调度更新时,更新操作会放在finally
中,返回去继续执行handlelick
的逻辑。于是会出现上面的情况。
看完这篇文章, 我们可以弄明白下面这几个问题:
为什么
setState
后不能马上拿到最新的state
的值?多个
setState
是如何合并的?setState
到底是同步还是异步的?为什么
setState
的值相同时,函数组件不更新?setState
是怎么完成更新的?useState
是什么时候初始化又是什么时候开始更新的?
评论