• React源码分析(三):useState,useReducer


    热身准备

    在正式讲useState,我们先热热身,了解下必备知识。

    为什么会有hooks

    大家都知道hooks是在函数组件的产物。之前class组件为什么没有出现hooks这种东西呢?

    答案很简单,不需要。

    因为在class组件中,在运行时,只会生成一个实例,而在这个实例中会保存组件的state等信息。在后续的更新操作中,也只是调用其中的render方法,实例中的信息不会丢失。而在函数组件中,每次渲染,更新都会去执行这个函数组件,所以在函数组件中是没办法保存state等信息的。为了保存state等信息,于是有了hooks,用来记录函数组件的状态,执行副作用。

    hooks执行时机

    上面提到,在函数组件中,每次渲染,更新都会去执行这个函数组件。所以我们在函数组件内部声明的hooks也会在每次执行函数组件时执行。

    在这个时候,可能有的同学听了我上面的说法(hooks用来记录函数组件的状态,执行副作用),又有疑惑了,既然每次函数组件执行都会执行hooks方法,那hooks是怎么记录函数组件的状态的呢?

    答案是,记录在函数组件对应的fiber节点中。

    两套hooks

    在我们刚开始学习使用hooks时,可能会有疑惑, 为什么hooks要在函数组件的顶部声明,而不能在条件语句或内部函数中声明?

    答案是,React维护了两套hooks,一套用来在项目初始化mount时,初始化hooks。而在后续的更新操作中会基于初始化的hooks执行更新操作。如果我们在条件语句或函数中声明hooks,有可能在项目初始化时不会声明,这样就会导致在后面的更新操作中出问题。

    hooks存储

    提前讲一下hooks存储方式,避免看晕了~~~

    每个初始化的hook都会创建一个hook结构,多个hook是通过声明顺序用链表的结构相关联,最终这个链表会存放在fiber.memoizedState中:

    var hook = {
        memoizedState: null,   // 存储hook操作,不要和fiber.memoizedState搞混了
        baseState: null,
        baseQueue: null,
        queue: null,    // 存储该hook本次更新阶段的所有更新操作
        next: null      // 链接下一个hook
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    而在每个hook.queue中存放的么个update也是一个链表结构存储的,千万不要和hook的链表搞混了。

    接下来,让我们带着下面几个问题看文章:

    1. 为什么setState后不能马上拿到最新的state的值?
    2. 多个setState是如何合并的?
    3. setState到底是同步还是异步的?
    4. 为什么setState的值相同时,函数组件不更新?

    假如我们有下面这样一段代码:

    function App(){
      const [count, setCount] = useState(0)
    
      const handleClick = () => {
        setCount(count => count + 1)
      }
    
      return (
        <div>
            勇敢牛牛,        <span>不怕困难</span>
            <span onClick={handleClick}>{count}</span>
        </div>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    初始化 mount

    useState

    我们先来看下useState()函数:

    function useState(initialState) {
      var dispatcher = resolveDispatcher();
      return dispatcher.useState(initialState);
    }
    
    • 1
    • 2
    • 3
    • 4

    上面的dispatcher就会涉及到开始提到的两套hooks的变换使用,initialState是我们传入useState的参数,可以是基础数据类型,也可以是函数,我们主要看dispatcher.useState(initialState)方法,因为我们这里是初始化,它会调用mountState方法:相关参考视频讲解:进入学习

    function mountState(initialState) {
      var hook = mountWorkInProgressHook();   // workInProgressHook
    
      if (typeof initialState === 'function') {
        // 在这里,如果我们传入的参数是函数,会执行拿到return作为initialState
        initialState = initialState();
      }
    
      hook.memoizedState = hook.baseState = initialState;
      var queue = hook.queue = {
        pending: null,
        dispatch: null,
        lastRenderedReducer: basicStateReducer,
        lastRenderedState: initialState
      };
      var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
      return [hook.memoizedState, dispatch];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    上面的代码还是比较简单,主要就是根据useState()的入参生成一个queue并保存在hook中,然后将入参和绑定了两个参数的dispatchAction作为返回值暴露到函数组件中去使用。

    这两个返回值,第一个hook.memoizedState比较好理解,就是初始值,第二个dispatch,也就是dispatchAction.bind(null, currentlyRenderingFiber$1, queue)这是个什么东西呢?
    我们知道使用useState()方法会返回两个值state, setState,这个setState就对应上面的dispatchAction,这个函数是怎么做到帮我们设置state的值的呢?

    我们先保留这个疑问,往下看,在后面会慢慢揭晓答案。

    接下来我们主要看看mountWorkInProgressHook都做了些什么。

    mountWorkInProgressHook

    function mountWorkInProgressHook() {
      var hook = {
        memoizedState: null,
        baseState: null,
        baseQueue: null,
        queue: null,
        next: null
      };
      // 这里的if/else主要用来区分是否是第一个hook
      if (workInProgressHook === null) {
        currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
      } else {
      //  把hook加到hooks链表的最后一条, 并且指针指向这条hook
        workInProgressHook = workInProgressHook.next = hook;  
      }
    
      return workInProgressHook;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    从上面的currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;这一行代码,我们可以发现,hook是存放在对应fiber.memoizedState上的。

    workInProgressHook = workInProgressHook.next = hook; ,从这一行代码,我们能知道,如果是有多个hook,他们是以链表的形式进行的存放。

    不仅仅是useState()这个hook会在初始化时走mountWorkInProgressHook方法,其他的hook,例如:useEffect, useRef, useCallback等在初始化时都是调用的这个方法。

    到这里我们能搞明白两件事:

    • hooks的状态数据是存放在对应的函数组件的fiber.memoizedState
    • 一个函数组件上如果有多个hook,他们会通过声明的顺序以链表的结构存储;

    到这里,我们的useState()已经完成了它初始化时的所有工作了,简单概括下,useState()在初始化时会将我们传入的初始值以hook的结构存放到对应的fiber.memoizedState,以数组形式返回[state, dispatchAction]

    更新 update

    当我们以某种形式触发setState()时,React也会根据setState()的值来决定如何更新视图。

    在上面讲到,useState在初始化时会返回[state, dispatchAction],那我们调用setState()方法,实际上就是调用dispatchAction,而且这个函数在初始化时还通过bind绑定了两个参数, 一个是useState初始化时函数组件对应的fiber,另一个是hook结构的queue

    来看下我精简后的dispatchAction(去除了和setState无关的代码)

    function dispatchAction(fiber, queue, action) {
      // 创建一个update,用于后续的更新,这里的action就是我们setState的入参
      var update = {
        lane: lane,
        action: action,
        eagerReducer: null,
        eagerState: null,
        next: null
      };
      // 这段闭环链表插入update的操作有没有很熟悉?
      var pending = queue.pending;
    
      if (pending === null) {
        update.next = update;
      } else {
        update.next = pending.next;
        pending.next = update;
      }
    
      queue.pending = update;
      var alternate = fiber.alternate;
        // 判断当前是否是渲染阶段
        if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
          var lastRenderedReducer = queue.lastRenderedReducer;
           // 这个if语句里的一大段就是用来判断我们这次更新是否和上次一样,如果一样就不会在进行调度更新
          if (lastRenderedReducer !== null) {
            var prevDispatcher;
    
            {
              prevDispatcher = ReactCurrentDispatcher$1.current;
              ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
            }
    
            try {
              var currentState = queue.lastRenderedState;
              var eagerState = lastRenderedReducer(currentState, action);
    
              update.eagerReducer = lastRenderedReducer;
              update.eagerState = eagerState;
    
              if (objectIs(eagerState, currentState)) {
                return;
              }
            } finally {
              {
                ReactCurrentDispatcher$1.current = prevDispatcher;
              }
            }
          }
        }
        // 将携带有update的fiber进行调度更新
        scheduleUpdateOnFiber(fiber, lane, eventTime);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    上面的代码已经是我尽力精简的结果了。。。代码上有注释,各位看官凑合看下。

    不愿细看的我来总结下dispatchAction做的事情:

    • 创建一个update并加入到fiber.hook.queue链表中,并且链表指针指向这个update
    • 判断当前是否是渲染阶段决定要不要马上调度更新;
    • 判断这次的操作和上次的操作是否相同, 如果相同则不进行调度更新;
    • 满足上述条件则将带有updatefiber进行调度更新;

    到这里我们又搞明白了一个问题:

    为什么setState的值相同时,函数组件不更新?

    updateState

    我们这里不详细讲解调度更新的过程, 后面文章安排, 这里我们只需要知道,在接下来更新过程中,会再次执行我们的函数组件,这时又会调用useState方法了。前面讲过,React维护了两套hooks,一套用于初始化, 一套用于更新。 这个在调度更新时就已经完成了切换。所以我们这次调用useState方法会和之前初始化有所不同。

    这次我们进入useState,会看到其实是调用的updateState方法

    function updateState(initialState) {
      return updateReducer(basicStateReducer);
    }
    
    • 1
    • 2
    • 3

    看到这几行代码,看官们应该就明白为什么网上有人说useStateuseReducer相似。原来在useState的更新中调用的就是updateReducer啊。

    updateReducer

    本来很长,想让各位看官忍一忍。于心不忍,忍痛减了很多

    function updateReducer(reducer, initialArg, init) {
      // 创建一个新的hook,带有dispatchAction创建的update
      var hook = updateWorkInProgressHook();
      var queue = hook.queue;
    
      queue.lastRenderedReducer = reducer;
      var current = currentHook;
    
      var baseQueue = current.baseQueue; 
      var pendingQueue = queue.pending;
    
      current.baseQueue = baseQueue = pendingQueue;
    
      if (baseQueue !== null) {
        // 从这里能看到之前讲的创建闭环链表插入update的好处了吧?直接next就能找到第一个update
        var first = baseQueue.next;
        var newState = current.baseState;
    
        var update = first;
        // 开始遍历update链表执行所有setState
        do {
          var updateLane = update.lane;
          // 假如我们这个update上有多个setState,在循环过程中,最终都会做合并操作
          var action = update.action;
          // 这里的reducer会判断action类型,下面讲
          newState = reducer(newState, action);
    
          update = update.next;
        } while (update !== null && update !== first);
    
        hook.memoizedState = newState;
        hook.baseState = newBaseState;
        hook.baseQueue = newBaseQueueLast;
        queue.lastRenderedState = newState;
      }
    
      var dispatch = queue.dispatch;
      return [hook.memoizedState, dispatch];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    上面的更新中,会循环遍历update进行一个合并操作,只取最后一个setState的值,这时候可能有人会问那直接取最后一个setState的值不是更方便吗?

    这样做是不行的,因为setState入参可以是基础类型也可以是函数, 如果传入的是函数,它会依赖上一个setState的值来完成更新操作,下面的代码就是上面的循环中的reducer

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

    到这里我们搞明白了一个问题,多个setState是如何合并的?

    updateWorkInProgressHook

    下面是伪代码,我把很多的逻辑判断给删除了,免了太长又让各位看官难受,原来的代码里会判断当前的hook是不是第一个调度更新的hook,我这里为了简单就按第一个来解析

    function updateWorkInProgressHook() {
      var nextCurrentHook;
    
      nextCurrentHook = current.memoizedState;
    
      var newHook = {
          memoizedState: currentHook.memoizedState,
          baseState: currentHook.baseState,
          baseQueue: currentHook.baseQueue,
          queue: currentHook.queue,
          next: null
          }
    
      currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
    
      return workInProgressHook;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    从上面代码能看出来,updateWorkInProgressHook抛去那些判断, 其实做的事情也很简单,就是基于fiber.memoizedState创建一个新的hook结构覆盖之前的hook。前面dispatchAction讲到会把update加入到hook.queue中,在这里的newHook.queue上就有这个update

    总结

    总结下useState初始化和setState更新:

    1. useState会在第一次执行函数组件时进行初始化,返回[state, dispatchAction]
    2. 当我们通过setState也就是dispatchAction进行调度更新时,会创建一个update加入到hook.queue中。
    3. 当更新过程中再次执行函数组件,也会调用useState方法,此时的useState内部会使用更新时的hooks
    4. 通过updateWorkInProgressHook获取到dispatchAction创建的update
    5. updateReducer通过遍历update链表完成setState合并。
    6. 返回update后的[newState, dispatchAction].

    还有两个问题

    1. 为什么setState后不能马上拿到最新的state的值? React其实可以这么做,为什么没有这么做,因为每个setState都会触发更新,React出于性能考虑,会做一个合并操作。所以setState只是触发了dispatchAction生成了一个update的动作,新的state会存储在update中,等到下一次render, 触发这个useState所在的函数组件执行,才会赋值新的state

    2. setState到底是同步还是异步的?
      同步的,假如我们有这样一段代码:

    const handleClick = () => {
      setCount(2)
      setCount(count => count + 1)
      console.log('after setCount')
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    你会惊奇的发现页面还没有更新count,但是控制台已经打印了after setCount

    之所以表现上像是异步,是因为内部使用了try{...}finally{...}。当调用setState触发调度更新时,更新操作会放在finally中,返回去继续执行handlelick的逻辑。于是会出现上面的情况。

    看完这篇文章, 我们可以弄明白下面这几个问题:

    1. 为什么setState后不能马上拿到最新的state的值?
    2. 多个setState是如何合并的?
    3. setState到底是同步还是异步的?
    4. 为什么setState的值相同时,函数组件不更新?
    5. setState是怎么完成更新的?
    6. useState是什么时候初始化又是什么时候开始更新的?
  • 相关阅读:
    基于jsp+mysql+ssm峰值预警停车场管理系统-计算机毕业设计
    Manage SQL Auditing from the Command Line
    “现在的自动驾驶太保守!”看看轻舟智航联合创始人大方这篇论文怎么说
    痞子衡嵌入式:理解i.MXRT中FlexSPI外设lookupTable里配置访问行列混合寻址Memory的参数值
    阿里云解决方案架构师张平:云原生数字化安全生产的体系建设
    HTTP协议(上)
    Oracle数据加载工具SQL* loader
    智慧会议解决方案-最新全套文件
    TCP的三次握手与四次挥手
    PostgreSQL备份工具pg_dump和pg_dumpall
  • 原文地址:https://blog.csdn.net/It_kc/article/details/127644784