这篇文章主要介绍“useState执行流程使怎样的”,在日常操作中,相信很多人在useState执行流程使怎样的问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”useState执行流程使怎样的”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
作为 React 开发者,你能答上如下两个问题么:
对于如下函数组件:
function App() { const [num, updateNum] = useState(0); window.updateNum = updateNum; return num; }
调用window.updateNum(1)
可以将视图中的0
更新为1
么?
对于如下函数组件:
function App() { const [num, updateNum] = useState(0); function increment() { setTimeout(() => { updateNum(num + 1); }, 1000); } return <p onClick={increment}>{num}</p>; }
在1秒内快速点击p
5次,视图上显示为几?
1. 可以 2. 显示为1
其实,这两个问题本质上是在问:
useState
如何保存状态?
useState
如何更新状态?
本文会结合源码,讲透如上两个问题。
这些,就是你需要了解的关于useState
的一切。
FunctionComponent
的render
本身只是函数调用。
那么在render
内部调用的hook
是如何获取到对应数据呢?
比如:
useState
获取state
useRef
获取ref
useMemo
获取缓存的数据
答案是:
每个组件有个对应的fiber节点
(可以理解为虚拟DOM
),用于保存组件相关信息。
每次FunctionComponent
render
时,全局变量currentlyRenderingFiber
都会被赋值为该FunctionComponent
对应的fiber节点
。
所以,hook
内部其实是从currentlyRenderingFiber
中获取状态信息的。
我们知道,一个FunctionComponent
中可能存在多个hook
,比如:
function App() { // hookA const [a, updateA] = useState(0); // hookB const [b, updateB] = useState(0); // hookC const ref = useRef(0); return <p></p>; }
那么多个hook
如何获取自己的数据呢?
答案是:
currentlyRenderingFiber.memoizedState
中保存一条hook
对应数据的单向链表。
对于如上例子,可以理解为:
const hookA = { // hook保存的数据 memoizedState: null, // 指向下一个hook next: hookB // ...省略其他字段 }; hookB.next = hookC; currentlyRenderingFiber.memoizedState = hookA;
当FunctionComponent
render
时,每执行到一个hook
,都会将指向currentlyRenderingFiber.memoizedState
链表的指针向后移动一次,指向当前hook
对应数据。
这也是为什么React
要求hook
的调用顺序不能改变(不能在条件语句中使用hook
) —— 每次render
时都是从一条固定顺序的链表中获取hook
对应数据的。
我们知道,useState
返回值数组第二个参数为改变state的方法。
在源码中,他被称为dispatchAction
。
每当调用dispatchAction
,都会创建一个代表一次更新的对象update
:
const update = { // 更新的数据 action: action, // 指向下一个更新 next: null };
对于如下例子
function App() { const [num, updateNum] = useState(0); function increment() { updateNum(num + 1); } return <p onClick={increment}>{num}</p>; }
调用updateNum(num + 1)
,会创建:
const update = { // 更新的数据 action: 1, // 指向下一个更新 next: null // ...省略其他字段 };
如果是多次调用dispatchAction
,例如:
function increment() { // 产生update1 updateNum(num + 1); // 产生update2 updateNum(num + 2); // 产生update3 updateNum(num + 3); }
那么,update
会形成一条环状链表。
update3 --next--> update1 ^ | | update2 |______next_______|
这条链表保存在哪里呢?
既然这条update
链表是由某个useState
的dispatchAction
产生,那么这条链表显然属于该useState hook
。
我们继续补充hook
的数据结构。
const hook = { // hook保存的数据 memoizedState: null, // 指向下一个hook next: hookForB // 本次更新以baseState为基础计算新的state baseState: null, // 本次更新开始时已有的update队列 baseQueue: null, // 本次更新需要增加的update队列 queue: null, };
其中,queue
中保存了本次更新update
的链表。
在计算state
时,会将queue
的环状链表剪开挂载在baseQueue
最后面,baseQueue
基于baseState
计算新的state
。
在计算state
完成后,新的state
会成为memoizedState
。
为什么更新不基于
memoizedState
而是baseState
,是因为state
的计算过程需要考虑优先级,可能有些update
优先级不够被跳过。所以memoizedState
并不一定和baseState
相同。
回到我们开篇第一个问题:
function App() { const [num, updateNum] = useState(0); window.updateNum = updateNum; return num; }
调用window.updateNum(1)
可以将视图中的0
更新为1
么?
我们需要看看这里的updateNum
方法的具体实现:
updateNum === dispatchAction.bind(null, currentlyRenderingFiber, queue);
可见,updateNum
方法即绑定了currentlyRenderingFiber
与queue
(即hook.queue
)的dispatchAction
。
上文已经介绍,调用dispatchAction
的目的是生成update
,并插入到hook.queue
链表中。
既然queue
作为预置参数已经绑定给dispatchAction
,那么调用dispatchAction
就步仅局限在FunctionComponent
内部了。
第二个问题
function App() { const [num, updateNum] = useState(0); function increment() { setTimeout(() => { updateNum(num + 1); }, 1000); } return <p onClick={increment}>{num}</p>; }
在1秒内快速点击p
5次,视图上显示为几?
我们知道,调用updateNum
会产生update
,其中传参会成为update.action
。
在1秒内点击5次。在点击第五次时,第一次点击创建的update
还没进入更新流程,所以hook.baseState
还未改变。
那么这5次点击产生的update
都是基于同一个baseState
计算新的state
,并且num
变量也还未变化(即5次update.action
(即num + 1
)为同一个值)。
所以,最终渲染的结果为1。
那么,如何5次点击让视图从1逐步变为5呢?
由以上知识我们知道,需要改变baseState
或者action
。
其中baseState
由 React 的更新流程决定,我们无法控制。
但是我们可以控制action
。
action
不仅可以传值
,也可以传函数
。
// action为值 updateNum(num + 1); // action为函数 updateNum(num => num + 1);
在基于baseState
与update
链表生成新state
的过程中:
let newState = baseState; let firstUpdate = hook.baseQueue.next; let update = firstUpdate; // 遍历baseQueue中的每一个update do { if (typeof update.action === 'function') { newState = update.action(newState); } else { newState = action; } } while (update !== firstUpdate)
可见,当传值
时,由于我们5次action
为同一个值,所以最终计算的newState
也为同一个值。
而传函数
时,newState
基于action
函数计算5次,则最终得到累加的结果。
如果这个例子中,我们使用useReducer
而不是useState
,由于useReducer
的action
始终为函数
,所以不会遇到我们例子中的问题。
事实上,useState
本身就是预置了如下reducer
的useReducer
。
function basicStateReducer(state, action) { return typeof action === 'function' ? action(state) : action; }
到此,关于“useState执行流程使怎样的”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。