Skip to content

关于useState #18

@jindada

Description

@jindada

关于useState

Q1: useState 如果保存状态?
Q2: useState 如何更新状态?

hook如何保存数据

FunctionComponentrender 只是函数调用。那么在 render 内部调用是如何获取到对应的数据?

例如:

  • useState 获取 state
  • useMemo 获取缓存数据

A: 每个组件都有个对应的 fiber 节点,用于保存组件的信息
每次 FunctionComponent render时,全局变量 currentlyRenderingFiber 都会被赋值为该 FunctionComponent 对应的 fiber 节点

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

多个hook如何保存数据

多个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 的调用顺序不能改变, 每次 render 时都是从一条固定顺序的链表中获取 hook 对应数据的

image

useState执行流程

const [ a, setA ] = useState();

setA(useState返回值的第二个参数) 是改变state的方法,在源码中被称为 dispatchAction, 每当调用 dispatchAction, 都会创建一个代表一次更新的对象 update:

例如:

function App() {
  const [ count, setCount ] = useState(0);
  
  function increment() {
    setCount(count + 1);
  }
  
  return <p onClick={increment}>{count}</p>;
}

调用 setCount(count + 1),会创建:

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

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

function increment() {
  // 产生update1
 setCount(num + 1);
  // 产生update2
 setCount(num + 2);
  // 产生update3
 setCount(num + 3);
}

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

image

既然这条 update 链表是由某个 useStatedispatchAction 产生,那么这条链表显然属于该 useState 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

image

为什么更新不基于memoizedState而是baseState,
是因为state的计算过程需要考虑优先级,可能有些update优先级不够被跳过。
所以memoizedState并不一定和baseState相同

下面我们看一个问题:

function App() {
  const [ count,  setCount ] = useState(0);
  window.setCount = setCount;
  return count;
}

调用 window.setCount(1) 可以将视图中的 0 更新为 1 么?
我们需要看看这里的 setCount 方法的具体实现:

setCount === dispatchAction.bind(null, currentlyRenderingFiber, queue);

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

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

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

update的action

function App() {
  const [ count, setCount ] = useState(0);
  
  function increment() {
    setTimeout(() => {
      setCount(count + 1);
    }, 1000);
  }
  
  return <p onClick={increment}>{count}</p>;
}

上面的结果是?

调用 setCount 会产生 update,其中传参会成为 update.action

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

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

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

useState与useReducer

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

我们知道,需要改变 baseState 或者 action

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

但是我们可以控制 action

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

// action为值
setCount(count+ 1);
// action为函数
setCount(count=> count + 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,由于 useReduceraction 始终为函数,所以不会遇到我们例子中的问题。

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

function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions