第三章:useState源码

207 阅读11分钟

前言

本章讲解useState源码,但不会贴太多源码,主要讲解其中的原理,会替换成通俗易懂的代码,以及其中有一些错误用法,还有常规下不会使用的骚操作。

useState 是 React 中的一个 Hook,用于在函数组件中添加状态。React 会触发组件的重新渲染,从而实现动态更新用户界面的效果。 useState 是 React 函数组件中实现状态管理的核心工具之一。这个是可以触发React重新渲染的Hook的其中之一。

源码讲解

如果只是简单了解可以阅读以下逻辑,如果想了解对应的源码:👉 [更详细源码]

关于useState部分Mount阶段:

调用useState方法时,先判断传入的参数是函数吗? 如果是函数将结果保存,不是函数直接保存,将返回一个数组,数组会有两个值,第一项是保存的结果,第二项是一个dispatchSetState函数,Mount阶段表现就完成,是不是很简单。[下面几条可以不用理解]

  • dispatchSetState bind一些参数,第一个参数是当前Fiber,第二个绑定参数是queue对象;
  • 初始化会创建一个hook对象,也是一个链表,next将指向下一个Hook,将hook对象挂载到Fiber.memoizedState上;
  • hook保存了当前state值,hook.memoizedState和hook.baseState;
  • hook.queue 更新队列

setState函数调用:

调用dispatchSetState函数,传入的参数,实际对应源码的第三个参数,将传入的参数保存下来,这里也可以传入一个函数,将当前state作为入参,返回结果作为新的state,如果多次调用setState,把每次结果都放在链表中保存下来, 判断最新的值和上次值是否相同,一样就什么也不做。不一样的话会调用更新。

关于state和任务队列部分源码对象(可以忽略)
fiber: {
    // 第一个useState
    memoizedState: {
        baseQueue: null,
        // useState 值
        baseState: baseState,
        // useState 值
        memoizedState: memoizedState,
        // 下一个useState
        next: null,
        // 更新队列
        queue: {
            // setState 函数
            dispatch: dispatchSetState.bind(null,fiber,queue),
            // 忽略
            interleaved: null,
            // 忽略
            lanes: 0,
            // 判断传入的参数是不是为函数,如果是函数,将lastRenderedState作为参数传递给函数,
            lastRenderedReducer:Function,
            // useState 值
            lastRenderedState: lastRenderedState ,
            // 更新队列
            pending:update,
        }
    }
}
// 更新的链表,是一个环状链表,每次调用setState一次,就会创建一个update对象,同时将next更新到尾部,并将当前next指向第一个update。
update: {
    // 当前的执行lane的值,可以先忽略,
    lane: lane,
    // 调用setState传递的参数
    action: action,
    // 判断是否已经处理结果完成最新State值
    hasEagerState: false,
    // 更新后最终的结果state,如果是函数会调用一次函数作为结果,否则==action
    eagerState: null,
    // 下一个更新Update
    next: update,
}



下面是上面的更详细解读,包含源码讲解

关于useState部分Update阶段:

本质上是调用updateReducer,实际上useState和useReducer更新阶段的核心代码是一份,拿到最后一次state值,并将更新到上次state位置,返回新的state数据和dispatch函数,dispatch在Mount和Update中是同一个。以上就是useState全部过程,是不是很简单。

源码讲解详细版,对标源码

关于useState部分的源码,实际上分为三个部分,构建阶段分为Mount和Update,以及更改setState的调用;

    const [state, setState] = useState(initialState)

Mount阶段

useState调用的是,HooksDispatcherOnMountInDEV.useState方法,接收一个参数initialState,useState函数内部前置会调用一些check方法,核心代码调用mountState(initialState) 调用mountWorkInProgressHook方法,创建一个workInProgressHook对象,workInProgressHook存在的值有以下几个:

const hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null
}

第一个useState会赋值给fiber.memoizedState,如果是非第一个useState,将workInProgressHook对象放在上一个workInProgressHook.next; initialState是函数立即执行,否则将initialState本身赋值hook.memoizedState = hook.baseState = initialState;
创建一个queue对象:

const queue = {
    pending: null,
    interleaved: null,
    lanes: 0dispatch: dispatchSetState.bind(null, fiber, queue),
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
}

赋值workInProgressHook.queue = queue
HooksDispatcherOnMountInDEV.useStatereturn值[workInProgressHook.memoizedState,queue.dispatch]
以上就是Mount阶段所有的代码,以下总结:

  • 往fiber.memoizedState上挂载数据;
  • 返回数组,一个值和一个方法;
setState(value);

调用setState方法执行了些什么: 先创建一个update对象

const update = {
    lane: lane, //这个涉及到lane模型,先忽略这个值的意义
    action: value,
    hasEagerState: false,
    eagerState: null,
    next: null,
}

update是一个环状链表,将queue.pedding = update
以下流程图指向:

graph TD; 
A[update1]--> B[update2]; B --> C[update3]; C --> A;

箭头表示next的指向, queue.pedding 等于最后一个upodate对象,只有一次setState就是update指向自身;
调用queue.lastRenderedState,解释一下basicStateReducer函数,basicStateReducer(queue.lastRenderedState, value) ,如果value是函数,将queue.lastRenderedState作为参数调用函数,结果返回,否则直接返回value;

update.eagerState = basicStateReducer(queue.lastRenderedState, value);
update.hasEagerState = true;

判断当前state和eagerState是否相同,用Object.is方法进行比较,一样就不做处理,不一样就scheduleUpdateOnFiber方法,简而言之就是让组件重新遍历更新;
如果setState中有一个值发生更改了,后面的setState就不会更新update的eagerState和hasEagerState,也就不会二次判断当前state和eagerState是否相同,一定会重新更新的。

以下总结:

  • 所有setState操作,都会创建update对象,update是个链表,放在queue.pending存储下来;
  • 放在queue.pending指向最后一个setState创建的update对象;
  • 截止到第一个有更新的setState前,都会更新eagerState和hasEagerState,后面的就不会更新;
  • 有第一个更新操作的setState,就一定会更新;

Update阶段

useState调用的是HooksDispatcherOnUpdateInDEV.useState方法,就是意味着在Mount阶段和Update阶段,useState时间上是调用了不同的函数,也是会调用一些check方法,核心代码是updateState(initialState),updateState方法就是调用了updateReducer(basicStateReducer),所以在Update阶段中initialState参数是没有作用的,而且在更新阶段中useState和useReducer是一份代码;
updateReducer第一步,将current fiber中fiber.memoizedState中浅克隆赋值给workInProgress fiber 上,next先设置null,判断一下是否有更新队列queue.pending != null
不为空,清空queue.pending队列,第一个更新操作是queue.pending.next,即第一个setState操作的update对象,判断这个update是否有执行过update.hasEagerState,没有执行的话就调用basicStateReducer执行,否则直接取值eagerState
循环执行update,都执行完成之后,将最后的一次的结果赋值:

queue.lastRenderedState = newState;
hook.memoizedState = newState;
hook.baseState = newState;

这样下一次更新的时候拿到baseState就是这次更新之后的值;
返回值 [hook.memoizedState, dispatch] 这样更新过程就完成了。

以下总结:

  • 先清空workInProgress fiber 上的 memoizedState,然后将current fiber的memoizedState浅拷贝给workInProgress fiber的 memoizedState上;
  • 拿到queue.pending.next ,作为第一个更新update;
  • 判断在setState阶段是否执行,没有执行执行一下,执行之后直接取eagerState值;
  • 遍历所有的update对象,将最后一个值更新到memoizedState等属性上,
  • 清空queue.pending
  • 返回最新的memoizedState和dispatch方法;

下图是将update阶段更新过程的数据对象汇总保存

补充一些额外知识

上面代码提到了在核心代码前会做一些check,其中Mount阶段会往hookTypesDev数组中放入当前的hook的名字,useState的名字就是useState,在Update阶段中会遍历hookTypesDev数组,如果发现Update阶段Hook链表上和使用的Hook名字和数组对应的名字不一致就会报错。
这就是大家常说的react hook必须于函数组件顶层使用和不能放在条件语句中的原因;
legacy.reactjs.org/docs/hooks-… 如果仅是单单解释其中原因,就没必要补充了,码农就是要玩一些骚操作:

案例零:

为什么我们使用useState会把解构的第二个参数变成set+第一个参数的驼峰命名,可不可以是其他的?

  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

答案是可以的,这么用只不过是大家约定而成的规范,并没有任何代码约束,useState会返回一个长度为2的数组,第一个是数据,第二个是可以改变数据的方法,我们习惯直接解构到两个新的变量上,这样别人阅读你的代码直接通过命名知道你代码的含义。

案例一:

根据源码检查机制,只要保证每次渲染中hook一样多,且name顺序一样,那么是不是就可以在条件语句中使用Hook了;

function Root(){
  const [childrenState, setChildrenState] = useState(1);
  let otherState
  if(childrenState == 1){
    // eslint-disable-next-line react-hooks/rules-of-hooks
    otherState = useState(2);
  }else {
    // eslint-disable-next-line react-hooks/rules-of-hooks
    otherState = useState(3);
  }
  const setStateFn = ()=>{
    console.log("方法点击事件")
    setChildrenState(1)
    setChildrenState(10)
    otherState[1](90)
  }
  console.log("childrenState = ", childrenState , "otherState = ", otherState[0])
  return <div onClick={()=>[
   
    setStateFn()
  ]}>{otherState[0]}</div>;
}

image.png 这是## ESLint Plugin的报错,不是js代码的问题,可以关闭Plugin插件,或者添加代码注释,绕过这个报错问题:

image.png 控制台打印出来:

image.png 看效果出来了,也是可以在条件语句中使用Hook,只不过是不推荐,但是可以这么骚操作,和正常使用方式的效果一样。
原理很简单,在Mount阶段,childrenState等于1,走useState == 2的分支,创建出一个otherState数组,值为2,调用click事件,会有一次更新,在Update阶段 childrenState != 1, 走useState == 3分支,js代码检查Hook name一致,通过,将current fiber中hook对象拷贝到workInProgress fiber中,更新update队列,更新最新值。

案例二:


之前面试遇到过问useState是同步还是异步的问题?
从上面源码解析可知,setState在同一个任务队列中,会把结果更新保存下来,然后一起更新,即使在setTimeout或者promise,宏任务或者微任务中一起更新,即先保存,后更新,为异步。
之前版本,记不太清了,更新可能是同步,在原生事件,setTimeout或者promise任务中就是同步的,而官方处理过的事件中就是异步的,这里只是简单介绍一下。

案例三:


function Root(){
  const [source, setSource] = useState(1);
  const [data, setData] = useState(2);
  const setStateFn = () => {
    console.log("click setStateFn")
    setSource(()=>{
      setData(4);
      return 3;
    })
    setSource(()=>{
      // setData(6);
      return 5;
    })
  }
  console.log(`state = ${source}, data = ${data}`)
  return <div onClick={setStateFn}>{data}</div>;
}

点击div之后会打印几次console,把setData(6)放开会打印几次:

image.png

image.png

为什么会出现这种情况,根据我们上面源码一点点推理, 在setState阶段,首先执行setSource中参数return 3,那个函数,在执行过程中,执行 setData(4) 之前,此时还没有state发生更改,Object.is(data,4)是不相等的,fiber添加个标识,后面的setState不会执行了,但是第一个setSource已经执行部分了,就会继续执行,把结果eagerState和标识hasEagerState更新;
之前已经发生state改变了,执行第二个setSource函数不会立即执行,会把函数保存下来,放在update链表上,在Update阶段才会执行;
Update阶段,遍历update链表,

  • 没有setData(6)代码时,最后一个setSource return 5,执行函数将5赋值给source state上,data state赋值为4,然后组件就会调用更新一次;
  • 有setData(6)代码时,更新过程中执行source的useState(1) 函数时更新update链表,在执行第二个setSource,会执行setData(6);,此时是Update阶段,但有setState操作,react添加一个标识符:didScheduleRenderPhaseUpdateDuringThisPass = true;,并将update更新到链表中,在更新data useState时链表已经有两个数据了,将6赋值给data,更新过程之后如果didScheduleRenderPhaseUpdateDuringThisPass是true的话会再一次调用这个函数组建,所以log会打印两次,
  • 第二次调用组件时,因为两个state的更新队列已经执行过了,会清空掉,所以这次只能就不会有更新update队列操作了。

为什么会添加didScheduleRenderPhaseUpdateDuringThisPass = true;操作,重新调用一次组件?
我个人理解,是为了保证结果的一致性,如果没有二次调用组件,在source和data state顺序不同时,导致结果不一致,如果source的state放在data的后面,先执行data,更新完任务队列 emoizedState结果是4,在更新source的任务队列中,往data里添加update值,可是data的state已经完成了,如果想重新更新data值只能在下一次Update阶段才能完成,性能会很差,所以重新调用一次组建,这样保证了数据的一致,性能也不会有问题。

总结

  1. 这些是react关于useState全部内容;
  2. 只能使用在函数组件中;
  3. 通过给对象赋值,实现在update阶段和mount阶段使用不同的函数;
  4. 在同一个任务队列中,更新是批次完成的,是异步更新;
  5. 以及汇总了一些骚操作;

留言

如果有什么好玩的,有趣的,不常见的,都可以欢迎留言,同时会更新到这个文章中,欢迎各位的见解。