usestate函数式更新_关于useState的一切

以下文章来源于魔术师卡颂 ,作者卡颂

魔术师卡颂

《React技术揭秘》作者,React Contributor

作为

React

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

对于如下函数组件:

function App() {

const [num, updateNum] = useState(0);

window.updateNum = updateNum;

return num;

}

调用

window.updateNum(1)

可以将视图中的

更新为

1

么?

对于如下函数组件:

function App() {

const [num, updateNum] = useState(0);

function increment() {

setTimeout(() => {

updateNum(num + 1);

}, 1000);

}

return

{num}

;

}

在1秒内快速点击

p

5次,视图上显示为几?

向右滑动展示答案 1. 可以

2. 显示为1

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

useState如何保存状态?useState如何更新状态?本文会结合源码,讲透如上两个问题。

这些,就是你需要了解的关于

useState

的一切。

hook如何保存数据

FunctionComponent

render

本身只是函数调用。

那么在

render

内部调用的

hook

是如何获取到对应数据呢?

比如:

useState获取stateuseRef获取refuseMemo获取缓存的数据答案是:

每个组件有个对应的

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

}

那么多个

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

{num}

;

}

调用

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技术揭秘[1]

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

function App() {

const [num, updateNum] = useState(0);

window.updateNum = updateNum;

return num;

}

调用

window.updateNum(1)

可以将视图中的

更新为

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

{num}

;

}

在1秒内快速点击

p

5次,视图上显示为几?

我们知道,调用

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

特性。

参考资料

[1]

React技术揭秘: https://react.iamkasong.com/state/priority.html#%E4%BB%80%E4%B9%88%E6%98%AF%E4%BC%98%E5%85%88%E7%BA%A7

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值