前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于useState的一切

关于useState的一切

作者头像
公众号@魔术师卡颂
发布2020-09-01 11:31:23
7630
发布2020-09-01 11:31:23
举报
文章被收录于专栏:魔术师卡颂魔术师卡颂

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

  1. 对于如下函数组件:
代码语言:javascript
复制
function App() {
  const [num, updateNum] = useState(0);
  window.updateNum = updateNum;
  return num;
}

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

  1. 对于如下函数组件:
代码语言:javascript
复制
function App() {
  const [num, updateNum] = useState(0);
  
  function increment() {
    setTimeout(() => {
      updateNum(num + 1);
    }, 1000);
  }
  
  return <p onClick={increment}>{num}</p>;
}

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

代码语言:javascript
复制
?向右滑动展示答案                                             1. 可以
                                                            2. 显示为1

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

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

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

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

hook如何保存数据

FunctionComponentrender本身只是函数调用。

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

比如:

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

答案是:

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

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

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

多个hook如何获取数据

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

代码语言:javascript
复制
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对应数据的单向链表。

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

代码语言:javascript
复制
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

代码语言:javascript
复制
const update = {
  // 更新的数据
  action: action,
  // 指向下一个更新
  next: null
};

对于如下例子

代码语言:javascript
复制
function App() {
  const [num, updateNum] = useState(0);
  
  function increment() {
    updateNum(num + 1);
  }
  
  return <p onClick={increment}>{num}</p>;
}

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

代码语言:javascript
复制
const update = {
  // 更新的数据
  action: 1,
  // 指向下一个更新
  next: null
  // ...省略其他字段
};

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

代码语言:javascript
复制
function increment() {
  // 产生update1
  updateNum(num + 1);
  // 产生update2
  updateNum(num + 2);
  // 产生update3
  updateNum(num + 3);
}

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

代码语言:javascript
复制
update3 --next--> update1
  ^                 |
  |               update2
  |______next_______|
                          

这条链表保存在哪里呢?

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

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

代码语言:javascript
复制
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]

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

代码语言:javascript
复制
function App() {
  const [num, updateNum] = useState(0);
  window.updateNum = updateNum;
  return num;
}

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

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

代码语言:javascript
复制
updateNum === dispatchAction.bind(null, currentlyRenderingFiber, queue);

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

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

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

update的action

第二个问题

代码语言:javascript
复制
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

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

但是我们可以控制action

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

代码语言:javascript
复制
// action为值
updateNum(num + 1);
// action为函数
updateNum(num => num + 1);

在基于baseStateupdate链表生成新state的过程中:

代码语言:javascript
复制
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,由于useReduceraction始终为函数,所以不会遇到我们例子中的问题。

事实上,useState本身就是预置了如下reduceruseReducer

代码语言:javascript
复制
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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-08-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 魔术师卡颂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • hook如何保存数据
  • 多个hook如何获取数据
  • useState执行流程
  • update的action
  • useState与useReducer
  • 总结
    • 参考资料
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档