关于useState的一切 - 知乎


本站和网页 https://zhuanlan.zhihu.com/p/200855720 的作者无关,不对其内容负责。快照谨为网络故障时之索引,不代表被搜索网站的即时页面。

关于useState的一切 - 知乎首发于前端八卦小报切换模式写文章登录/注册关于useState的一切魔术师卡颂技术博主,《React设计原理》作者32 人赞同了该文章作为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秒内快速点击p5次,视图上显示为几? 向右滑动展示答案 1. 可以
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,比如: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执行流程我们知道,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相同。更详细的解释见React技术揭秘 回到我们开篇第一个问题: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内部了。update的action第二个问题function App() {
const [num, updateNum] = useState(0);
function increment() {
setTimeout(() => {
updateNum(num + 1);
}, 1000);
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不仅可以传值,也可以传函数。// 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的完整执行过程。本系列文章接下来会继续以实例 + 源码的方式,解读业务中经常使用的React特性。 开源电子书《React技术揭秘》,轻松学懂React源码 发布于 2020-08-27 13:05React源代码Function​赞同 32​​4 条评论​分享​喜欢​收藏​申请转载​文章被以下专栏收录前端八卦小报网罗全球不可靠来源前端八卦