likes
comments
collection
share

从源码的角度告诉你 《为啥2次传入setState的值相同,函数组件不更新?》

作者站长头像
站长
· 阅读数 23

大家好,我是小九九的爸爸,今天我们来分析一道面试题:《两次传入useState的值相同,函数组件为啥不更新?》。大家准备好了嘛,我要发车了。

一、现象描述

代码如下:

function FC(){
    // 这里需要做出一些定义,我们将使用 `setHook` 来代替useState返回的数组里的第二个参数
    // 在本例中,setHook就是 setState0、setState1的代名词
    let [state0, setState0] = useState(0);
    let [state1, setState1] = useState(1);

    const clickButton = () => {
        setState0((value) => {
            return 0;
        });
    }
    return <div>
        内容:{state0}
        <div>
            随机数:{Math.random()*100}
        </div>
        <button onClick={clickButton}>+1</button>
    </div>
}

上述代码对应的现象是下面这样:

从源码的角度告诉你 《为啥2次传入setState的值相同,函数组件不更新?》

二、你将学到的知识

1、函数组件内部是如何维护状态的?

2、函数组件内部是如何判断是否要更新的?

3、useState的返回值为什么是数组?

三、useState源码分析

函数组件是如何通过useState来保存状态的?

先直接告诉结论吧:一个组件内部如果多次调用 useState,那么就会在组件内部 依次创建相应层次来保存state对象,这些层次通过next属性连接成一条链

useState初始化时的源码分析

从源码的角度告诉你 《为啥2次传入setState的值相同,函数组件不更新?》

第一次调用useState

第一步:创建state对象

函数式组件通过调用 mountWorkInProgressHook()来创建state对象,其源码如下:

function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

在上述代码中,currentlyRenderingFiber 代表当前正在渲染的组件对象,当我们第一次渲染<FC/>组件,并且代码执行到 useState(0) 时,mountWorkInProgressHook里的代码是这样的:


function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  }
  return workInProgressHook;
}

此时 currentlyRenderingFiber 对象是这样:

/**
    currentlyRenderingFiber: {
        memoizedState: {
            memoizedState: null,
            baseState: null,
            baseQueue: null,
            queue: null,
            next: null
        }
    }
*/

很好,目前我们已经知道了state对象是如何创建的了,那么我们接着看一下 mountState 剩余的逻辑。

第二步:保存initState

这部分代码比较直观,请直接看代码里的注释即可。

function mountState(initialState) {

  // 1、创建state变量 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  var hook = mountWorkInProgressHook();
  
  /**
      执行完上一步后,各变量的watch情况:
      
      hook: {                                        currentlyRenderingFiber:{
        memoizedState: null,                             memoizedState:{
        baseState: null,                                     memoizedState:null,
        baseQueue: null,                                     baseState:null,
        queue: null,                                         baseQueue:null,
        next: null                                           queue:null,
      }                                                      next:null
                                                         }
                                                     }
  */
  
  
  // 2、保存initState、以及各变量赋值 ++++++++++++++++++++++++++++++++++++++++++++
  hook.memoizedState = hook.baseState = initialState;
  
  /**
     执行完上一步后,各变量的watch情况:
     
     hook:{                                           currentlyRenderingFiber:{
         memoizedState: 0,                                memoizedState: {
         baseState: 0,                                        memoizedState: 0,
         baseQueue: null,                                     baseState: 0,
         queue: null,                                         baseQueue: null,
         next: null                                           queue: null,
     }                                                        next: null
                                                          }
                                                       }
  */                    
  
  const queue = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: 0,
  };
  hook.queue = queue;
  
  /**
      执行完上一步后,各变量的watch情况:
      
      hook:{                                            currentlyRenderingFiber:{
         memoizedState: 0,                                   memoizedState:{
         baseState: 0,                                           memoizedState: 0,
         baseQueue: null,                                        baseState: 0,
         queue: {                                                baseQueue: null,
            pending: null,                                       queue: {
            interleaved: null,                                       pending: null,
            lanes: NoLanes,                                          interleaved: null,
            dispatch: null,                                          lanes: NoLanes,
            lastRenderedReducer: basicStateReducer,                  dispatch: null,
            lastRenderedState: 0,                                    lastRenderedReducer: basicStateReducer,
                                                                     lastRenderedState: 0
         },                                                      },
         next: null                                              next: null
      }                                                      }
                                                          }
  */
  
  // 3、将state与action绑定 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  const dispatch = (queue.dispatch = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  )));
  return [hook.memoizedState, dispatch];
}
第三步:将state与action绑定

这里我们需要提前做一下setHook的定义,

这一步主要是通过调用 dispatchSetState 函数来完成,这一部分主要代码如下:

/**
    dispatchSetState函数形参里的action的值取决于setHook里的参数,比如:
    
    let [count, setCount] = useState(0);
    
    setCount(1)  -->  action就是1
    setCount( () => 1 ) -->  action就是 () => 1 这个函数
*/
function dispatchSetState( fiber, queue, action ){
    // 省略其余代码 =================
    const update = {
        lane,
        action,
        hasEagerState: false,
        eagerState: null,
        next: null,
     };
     var currentState = queue.lastRenderedState;
     var eagerState = lastRenderedReducer(currentState, action);
     if (Object.is(eagerState, currentState)) {
        return;
     }
     // 省略其余代码 ===============
}

结合上面的源代码,我们发现setHook就是dispatchdispatch就是dispatchSetState。在这个函数里,我们会拿到组件内部的当前状态(currentState)即将要改变的下一个状态(eagerState)来做Object.is浅比较,如果2个状态相等,就直接返回,任何事情都不会做,这也是为啥多次setState的值相同,但是组件不更新的原因。

第四步:为什么useState返回的是一个数组?

这个问题也是面试中会被考的一个点。针对这个点,我们首先是想,可以返回什么样的数据结构?

根据目前的框架使用情况来看的话,似乎返回格式只能是 数组或者对象

那么 数组解构 相比 对象解构 有哪些优势呢?

我觉着 变量名自定义 这是 数组解构 能够取胜的绝对原因。这毕竟降低了用户的学习成本。增强使用者代码的可读性。

第二次调用useState

还记得我们的 <FC/>组件 的代码嘛,里面调用了2次useState

function Fc(){
    let [state0, setState0] = useState(0);
    let [state1, setState1] = useState(1);
    ...
}

当我们第一次调用useState(0)的时候,组件内部的state状态是这样维护的:

/**
    currentlyRenderingFiber: {
        memoizedState: {
            memoizedState: 0,
            baseState: 0,
            baseQueue: null,
            queue: {
                pending: null,
                interleaved: null,
                lanes: NoLanes,
                dispatch: null,
                lastRenderedReducer: basicStateReducer,
                lastRenderedState: 0
            },
            next: null
        }
    }
*/

当我们第二次调用 useState(1) 的时候,实际上就是重新 走了一遍第一次调用useState(0),唯一的区别就是创建state对象的过程中稍微不一样而已,我们现在一起来看下:

function mountWorkInProgressHook(){
  const hook= {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // 组件内部第一次执行useState(0)的时候会走这里
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // 组件内部第二次执行useState(1)的时候会走这里
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

workInProgressHook = workInProgressHook.next = hook; 这行代码可了不得,做了2件事,我们先把这行代码拆解一下:

workInProgressHook.next = hook;
workInProgressHook = hook;

2件事如下:

  • 将后续调用useState时创建的state对象通过next属性连成链。
  • workInProgressHook始终指向最新的state对象。

第二次调用useState(1)后,此时 currentlyRenderingFiberworkInProgressHook 的value情况如下:

/**
    currentlyRenderingFiber:{
        memoizedState: {
            memoizedState: 0,
            baseState: 0,
            baseQueue: null,
            queue: {
                pending: null,
                interleaved: null,
                lanes: NoLanes,
                dispatch: null,
                lastRenderedReducer: basicStateReducer,
                lastRenderedState:0
            }
            next: {
                memoizedState: 1,
                baseState: 1,
                baseQueue: null,
                queue: {
                    pending: null,
                    interleaved: null,
                    lanes: NoLanes,
                    dispatch: null,
                    lastRenderedReducer: basicStateReducer,
                    lastRenderedState: 1
                },
                next: null
            }
       }
    }
    
    workInProgressHook: {
        memoizedState: 1,
        baseState: 1,
        baseQueue: null,
        queue: {
            pending: null,
            interleaved: null,
            lanes: NoLanes,
            dispatch: null,
            lastRenderedReducer: basicStateReducer,
            lastRenderedState: 1
        },
        next: null
    }
*/

四、最后

好啦,本篇文章到这里就结束啦,如果在上述过程中发现任何问题,欢迎评论区里指正。同时,如果您觉得还不错,还请给一个小赞赞~~

转载自:https://juejin.cn/post/7240297635427663930
评论
请登录