关于useState的一切

开发 前端
通过本文,我们了解了useState的完整执行过程。一起来看一下吧。

作为React开发者,你能答上如下两个问题么:

    1.  对于如下函数组件: 

  1. function App() {  
  2.   const [num, updateNum] = useState(0);  
  3.   window.updateNum = updateNum; 
  4.   return num;  

调用window.updateNum(1)可以将视图中的0更新为1么?

    2.  对于如下函数组件: 

  1. function App() {  
  2.   const [num, updateNum] = useState(0);  
  3.   function increment() {  
  4.     setTimeout(() => {  
  5.       updateNum(num + 1);  
  6.     }, 1000);  
  7.   }  
  8.   return <p onClick={increment}>{num}</p> 

在1秒内快速点击p5次,视图上显示为几? 

  1. 👉向右滑动展示答案                                             1. 可以  
  2.                                                             2. 显示为1 

其实,这两个问题本质上是在问:

  •  useState如何保存状态?
  •  useState如何更新状态?

本文会结合源码,讲透如上两个问题。

这些,就是你需要了解的关于useState的一切。

hook如何保存数据

FunctionComponent的render本身只是函数调用。

那么在render内部调用的hook是如何获取到对应数据呢?

比如:

  •  useState获取state
  •  useRef获取ref
  •  useMemo获取缓存的数据

答案是:

每个组件有个对应的fiber节点(可以理解为虚拟DOM),用于保存组件相关信息。

每次FunctionComponent render时,全局变量currentlyRenderingFiber都会被赋值为该FunctionComponent对应的fiber节点。

所以,hook内部其实是从currentlyRenderingFiber中获取状态信息的。

多个hook如何获取数据

我们知道,一个FunctionComponent中可能存在多个hook,比如: 

  1. function App() {  
  2.   // hookA  
  3.   const [a, updateA] = useState(0);  
  4.   // hookB 
  5.   const [b, updateB] = useState(0);  
  6.   // hookC  
  7.   const ref = useRef(0); 
  8.    return <p></p> 

那么多个hook如何获取自己的数据呢?

答案是:

currentlyRenderingFiber.memoizedState中保存一条hook对应数据的单向链表。

对于如上例子,可以理解为: 

  1. const hookA = {  
  2.   // hook保存的数据  
  3.   memoizedState: null,  
  4.   // 指向下一个hook  
  5.   next: hookB  
  6.   // ...省略其他字段  
  7. }; 
  8. hookB.next = hookC 
  9. currentlyRenderingFiber.memoizedState = hookA

当FunctionComponent render时,每执行到一个hook,都会将指向currentlyRenderingFiber.memoizedState链表的指针向后移动一次,指向当前hook对应数据。

这也是为什么React要求hook的调用顺序不能改变(不能在条件语句中使用hook) —— 每次render时都是从一条固定顺序的链表中获取hook对应数据的。

useState执行流程

我们知道,useState返回值数组第二个参数为改变state的方法。

在源码中,他被称为dispatchAction。

每当调用dispatchAction,都会创建一个代表一次更新的对象update: 

  1. const update = {  
  2.   // 更新的数据  
  3.   action: action,  
  4.   // 指向下一个更新  
  5.   next: null  
  6. }; 

对于如下例子 

  1. function App() {  
  2.   const [num, updateNum] = useState(0);  
  3.   function increment() {  
  4.     updateNum(num + 1);  
  5.   }  
  6.   return <p onClick={increment}>{num}</p> 

调用updateNum(num + 1),会创建: 

  1. const update = {  
  2.   // 更新的数据  
  3.   action: 1,  
  4.   // 指向下一个更新  
  5.   next: null  
  6.   // ...省略其他字段  
  7. }; 

如果是多次调用dispatchAction,例如: 

  1. function increment() {  
  2.   // 产生update1  
  3.   updateNum(num + 1);  
  4.   // 产生update2  
  5.   updateNum(num + 2);  
  6.   // 产生update3  
  7.   updateNum(num + 3);  }

那么,update会形成一条环状链表。 

  1. update3 --next--> update1  
  2.   ^                 |  
  3.   |               update2  
  4.   |______next_______|                       

 这条链表保存在哪里呢?

既然这条update链表是由某个useState的dispatchAction产生,那么这条链表显然属于该useState hook。

我们继续补充hook的数据结构。 

  1. const hook = {  
  2.   // hook保存的数据  
  3.   memoizedState: null,  
  4.   // 指向下一个hook  
  5.   next: hookForB  
  6.   // 本次更新以baseState为基础计算新的state  
  7.   baseState: null,  
  8.   // 本次更新开始时已有的update队列 
  9.    baseQueue: null,  
  10.   // 本次更新需要增加的update队列  
  11.   queue: null,  
  12. }; 

其中,queue中保存了本次更新update的链表。

在计算state时,会将queue的环状链表剪开挂载在baseQueue最后面,baseQueue基于baseState计算新的state。

在计算state完成后,新的state会成为memoizedState。

为什么更新不基于memoizedState而是baseState,是因为state的计算过程需要考虑优先级,可能有些update优先级不够被跳过。所以memoizedState并不一定和baseState相同。更详细的解释见React技术揭秘[1]

回到我们开篇第一个问题: 

  1. function App() {  
  2.   const [num, updateNum] = useState(0);  
  3.   window.updateNum = updateNum;  
  4.   return num;  

调用window.updateNum(1)可以将视图中的0更新为1么?

我们需要看看这里的updateNum方法的具体实现: 

  1. updateNum === dispatchAction.bind(null, currentlyRenderingFiber, queue); 

可见,updateNum方法即绑定了currentlyRenderingFiber与queue(即hook.queue)的dispatchAction。

上文已经介绍,调用dispatchAction的目的是生成update,并插入到hook.queue链表中。

既然queue作为预置参数已经绑定给dispatchAction,那么调用dispatchAction就步仅局限在FunctionComponent内部了。

update的action

第二个问题 

  1. function App() {  
  2.   const [num, updateNum] = useState(0); 
  3.    function increment() {  
  4.     setTimeout(() => {  
  5.       updateNum(num + 1);  
  6.     }, 1000);  
  7.   }  
  8.   return <p onClick={increment}>{num}</p> 

在1秒内快速点击p5次,视图上显示为几?

我们知道,调用updateNum会产生update,其中传参会成为update.action。

在1秒内点击5次。在点击第五次时,第一次点击创建的update还没进入更新流程,所以hook.baseState还未改变。

那么这5次点击产生的update都是基于同一个baseState计算新的state,并且num变量也还未变化(即5次update.action(即num + 1)为同一个值)。

所以,最终渲染的结果为1。

useState与useReducer

那么,如何5次点击让视图从1逐步变为5呢?

由以上知识我们知道,需要改变baseState或者action。

其中baseState由React的更新流程决定,我们无法控制。

但是我们可以控制action。

action不仅可以传值,也可以传函数。 

  1. // action为值  
  2. updateNum(num + 1);  
  3. // action为函数  
  4. updateNum(num => num + 1); 

在基于baseState与update链表生成新state的过程中: 

  1. let newState = baseState 
  2. let firstUpdate = hook.baseQueue.next;  
  3. let update = firstUpdate
  4. // 遍历baseQueue中的每一个update  
  5. do {  
  6.   if (typeof update.action === 'function') {  
  7.     newState = update.action(newState);  
  8.   } else {  
  9.     newState = action 
  10.   }  
  11. } while (update !== firstUpdate) 

可见,当传值时,由于我们5次action为同一个值,所以最终计算的newState也为同一个值。

而传函数时,newState基于action函数计算5次,则最终得到累加的结果。

如果这个例子中,我们使用useReducer而不是useState,由于useReducer的action始终为函数,所以不会遇到我们例子中的问题。

事实上,useState本身就是预置了如下reducer的useReducer。 

  1. function basicStateReducer(state, action) {  
  2.   return typeof action === 'function' ? action(state) : action;  

总结

通过本文,我们了解了useState的完整执行过程。 

 

责任编辑:庞桂玉 来源: 前端大全
相关推荐

2021-02-28 09:47:54

软件架构软件开发软件设计

2021-02-19 23:08:27

软件测试软件开发

2018-11-23 11:17:24

负载均衡分布式系统架构

2020-10-14 08:04:28

JavaScrip

2021-05-28 07:12:59

Python闭包函数

2022-08-21 17:35:31

原子多线程

2023-04-20 10:15:57

React组件Render

2018-01-17 09:15:52

负载均衡算法

2023-04-12 14:04:48

光纤网络

2022-08-17 06:25:19

伪共享多线程

2022-04-02 09:38:00

CSS3flex布局方式

2023-02-10 08:44:05

KafkaLinkedIn模式

2023-07-10 10:36:17

人工智能AI

2018-01-05 14:23:36

计算机负载均衡存储

2021-08-09 14:40:02

物联网IOT智能家居

2018-06-15 23:00:56

2022-07-15 14:58:26

数据分析人工智能IT

2021-12-29 14:24:12

物联网IoT5G

2022-12-30 11:24:21

2022-04-24 09:00:00

渗透测试安全数字时代
点赞
收藏

51CTO技术栈公众号