useState的源码解析

208 阅读8分钟

useState的源码解析

从全面拥抱hooks后,我们在代码中经常使用useState的钩子,再使用的过程中,我有一些思考点,好奇 react内部是怎么实现的,于是就简单的研究了一下react的useState源码部分。

思考点

带着思考点去研究源码,可以让我们有明确的目标。

  1. 初始化的时候,传入useState的初始值怎么返回的
  2. 组件重新渲染后,不是当前useStateset触发的,是怎么返回初始化的值的
  3. 设置新的值后,是怎么返回最新的值,而不返回初始化的值的

名词说明

current: 当前正在页面渲染的node,如果是第一次渲染,则为空

workInProgress: 新的node,用于下一次页面的渲染更新

component: node对应的组件

currentlyRenderingFiber$1 = workInProgress (在renderWithHooks中执行)

// 函数组件中的memoizeState
interface State {
 memoizedState: state数据,和baseState值相同,
 baseState: state数据,
 baseQueue: 本次更新之前没执行完的queue,
 next: 下一个state,
 queue: {
   pending: 更新state数据(这个数据是一个对象,里面有数据,还有其他key用于做其他事情。),
   dispatch: setState方法本身,
   lastRenderedReducer: useReducer用得上,
   lastRenderedState: 上次渲染的State.memoizedState数据,
 }
}

代码片段

我们研究的代码片段是下面这样的,使用了2个useState,分别是countname , 然后有2个更新值的按钮

function Index() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('yx')
  return (
      <div>
        <p>count: {count}</p>
        <p>{name}</p>
        <button onClick={() => {
          setName(name =>  name + 'y')
        }}>change name</button>
        <button onClick={() => {
          setCount(count =>  count + 1)
        }}>change count</button>
      </div>
  )
}

源码解析

我们基于上面的代码片段,将流程分为2个大部分,初始化和更新。分别探讨useState在初始化和更新表现。

react对于钩子函数(hooks)分为几个阶段,每一个阶段调用的hooks的函数都不同。

  1. 在组件初始化的时候,调用的是HooksDispatcherOnMountInDEV下的钩子
  2. 在更新的时候,调用的是HooksDispatcherOnUpdateInDEV下的钩子

初始化

在初始化的时候,调用useState实际上是调用HooksDispatcherOnMountInDEV下面的useState, 我们只关注主要流程。通过下面代码可以看出,实际上是调用了mountState(initialState)函数, 并传入初始化的值。

HooksDispatcherOnMountInDEV = { 
    useState: function (initialState) {
      currentHookNameInDev = 'useState';
      mountHookTypesDev();
      var prevDispatcher = ReactCurrentDispatcher$1.current;
      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
      try {
        return mountState(initialState);
      } finally {
        ReactCurrentDispatcher$1.current = prevDispatcher;
      }
    },
}

mountState()函数主要是执行了一些初始化的操作,主要的是分为几步。

  1. 执行mountWorkInProgressHook函数
  2. 赋值初始化的值到当前组件的fiber中
  3. 包装dispatch函数
function mountState(initialState) {
  var hook = mountWorkInProgressHook();

  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    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];
}
mountWorkInProgressHook的执行
  1. 执行mountWorkInProgressHook() 在当前组件的fiber上挂载一个memoizedState属性,记录初始化的值、下一个钩子的链表以及初始化workInProgressHook
function mountWorkInProgressHook () {
    currentFiber.memoizedState = workInProgressHook = {
      memoizedState: null, // 初始化的值
      baseState: null, 
      baseQueue: null,
      queue: null, // 包含有待更新的值
      next: null // 记录下一个钩子函数
    };
    return workInProgressHook
}
总结

从上面的代码中,我们可以看出,useState初始化的时候,主要是执行了mountState函数,然后将传入初始化的值挂载当前组件的fiber的memoizedState, 并直接返回,这样我就得到了需要渲染的值, 至此初始化的操作就全部完成。


更新其他的state

当我们点击change name按钮 的时候,执行的是setName, 并没有修改count的值。当组件重新渲染的时候,useState(count)是怎么返回的值呢? 由于已经初始化过了,当我们执行完name的dispatch后。组件重新渲染,当再次执行到useState(count)的时候,经历了如下逻辑:

  1. 在更新阶段执行的是HooksDispatcherOnUpdateInDEV下面的useState方法, 主要是保留dispatch方法和执行updateState(initialState)并传入初始化的值
HooksDispatcherOnUpdateInDEV = {
    useState: function (initialState) {
      currentHookNameInDev = 'useState';
      var prevDispatcher = ReactCurrentDispatcher$1.current;
      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
      try {
        return updateState(initialState);
      } finally {
        ReactCurrentDispatcher$1.current = prevDispatcher;
      }
    },
}
  1. 接下来我们看看updateState()做了什么操作。它主要是执行了updateReducer方法, 并传入了一个basicStateReducer函数。
function updateState(initialState) {
  return updateReducer(basicStateReducer);
}
function basicStateReducer(state, action) {
  // $FlowFixMe: Flow doesn't like mixed types
  return typeof action === 'function' ? action(state) : action;
}
  1. 看来我们的重点应该是updateReducer()函数的执行中做了什么操作。
updateReducer()做了什么
  1. 执行了updateWorkInProgressHook()函数,返回一个hook。这个函数主要是通过react的双缓存树,获取到当前hooks记录的状态。文章最后有这个函数的执行分析,我们先只关注返回的值hook
function updateReducer(reducer, initialArg, init) {
    /*
      hook = {
        baseQueue: null
        baseState: 0
        memoizedState: 0
        next: null
        queue: {
            dispatch: ƒ ()
            lastRenderedReducer: ƒ basicStateReducer(state, action)
            lastRenderedState: 0
            pending: null
        }
      }
    */
   var hook = updateWorkInProgressHook();  
   var queue = hook.queue; 
   var current = currentHook; // The last rebase update that is NOT part of the base state.

   var baseQueue = current.baseQueue; // The last pending update that hasn't been processed yet.

   var pendingQueue = queue.pending;
   if(pendingQueue !== null) {
     ....
   } 
   if(baseQueue !== null) {
     ...
   }
   var dispatch = queue.dispatch;
   return [hook.memoizedState, dispatch];
}
  1. 取出currentHook有没有baseQueue或者hook下面的queuependingQueue值,由于我们没有修改count的值,只修改了name的值,所以pendingQueuebaseQueue都是null, 直接就返回了之前记录初始化的值,并没有做什么改变。
总结

从上面我们可以看出,当更新流程不是当前的useState触发的时候,由于没有对应的更新队列,其实react内部并没有做什么操作,直接返回了初始化的时候记录在组件fiber中的memoizedState值。


更新对应的state

如果我们执行的是setCount的时候, 修改了count的值,那useState的执行逻辑是什么样的呢?

  1. 第一步还是肯定是执行更新updateWorkInProgressHook下面的useState
  2. 从上面的介绍,我们知道主要的流程还是在执行updateReducer()后。

updateReducer()分析

  1. 进入updateWorkInProgressHook()获取hook,我们得到类似这样的一个结构的hook和currentHook, 用于之后的updateReducer中分析
   // 其实当前的hook就是把currentHook的next的值设置为null
   let hook = {  
       baseQueue: null,
       baseState: 0,  // 上一次的值
       memoizedState: 0, // 上一次的值
       next: null,
       queue: {
         lastRenderedState: 0,
         lastRenderedReducer: basicStateReducer,
         dispatch: func,
         pending: { // 待更新的队列 
             action: ƒ (count),
             eagerReducer: ƒ basicStateReducer(state, action),
             eagerState: 1,  // 新的值
             lane: 1, // 更新优先级
             next: {
                   action: ƒ (count),
                   eagerReducer: ƒ basicStateReducer(state, action),
                   eagerState: 1,
                   lane: 1,
                   next: ....  
            }
         }
       }
   }
 let currentHook = {  
   baseQueue: null,
   baseState: 0,  // 上一次的值
   memoizedState: 0, // 上一次的值
   next: {
       baseQueue: null,
       baseState"yx",
       memoizedState"yx",
       next: null, //最后一个钩子
       queue: { // 更新队列
           pending: null,
           ...
       }
   },
   queue: {
       lastRenderedState: 0,
       lastRenderedReducer: basicStateReducer,
       dispatch: func,
       pending: { // 待更新的队列 
         action: ƒ (count),
         eagerReducer: ƒ basicStateReducer(state, action),
         eagerState: 1,  // 新的值
         lane: 1, // 更新优先级
         next: {
             action: ƒ (count),
             eagerReducer: ƒ basicStateReducer(state, action),
             eagerState: 1,
             lane: 1,
             next: ....   //重复
          }
      }
   }
}
  1. 执行完updateWorkInProgressHook后,我们得到了一个hook对象以及currentHook的值,重新进入到updateReducer中。
   function updateReducer(reducer, initialArg, init) {
     var hook = updateWorkInProgressHook(); // 得到一个更新链表
     var queue = hook.queue;
     
     var current = currentHook; // 当前的队列
   
     var baseQueue = current.baseQueue; // 此时当前的队列为null 
   
     var pendingQueue = queue.pending; // 找到hook下面的queue更新队列
   
     // 进入条件,将pendingQueue同步到baseQueue
     if (pendingQueue !== null) {
       current.baseQueue = baseQueue = pendingQueue;
       queue.pending = null; // 同步更新后,重置掉pending
     }
   
     // 有待更新的队列,进入更新
     if (baseQueue !== null) {
       var first = baseQueue.next; // 获取到准备更新到队列 {eagerState: 1}
       var newState = current.baseState; // 获取到当前的值
       var newBaseState = null;
       var newBaseQueueFirst = null;
       var newBaseQueueLast = null;
       var update = first;
       do {
         var updateLane = update.lane;
         if (update.eagerReducer === reducer) {
             // If this update was processed eagerly, and its reducer matches the
             // current reducer, we can use the eagerly computed state.
             newState = update.eagerState; // 此时newState的值为1
           } else {
             var action = update.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;
     }。
     // 得到新的值并返回给useState
     var dispatch = queue.dispatch;
     return [hook.memoizedState, dispatch];
   }
  1. 执行完updateReducer后得到最新的值返回,到这里就全部结束了。其他部分都是双缓存树进行对比然后进行虚拟dom渲染过程。

总结

到这里,我们就明白了,如果更新的是当前的state,主要是通过updateWorkInProgressHook获取当前的hook以及currentHook,之后再次进入updateReducer去计算值。

updateReducer中主要是通过是否有待更新的队列pendingQueue(来自于hook中的queue)来判断流程,如果没有,就直接返回了一开始记录的值。如果有,就同步到baseQueue中,然后根据baseQueue, 通过一个while去计算值,然后更新值到组件fiber的memoizedState值上。


更多

在看源码的过程中,我发现了很多钩子在执行的过程中都执行了下面的这个代码。在updateReducer中也看到了currentHook,我们来分析一下updateWorkInProgressHook的内部执行过程。

var hook = updateWorkInProgressHook();

updateWorkInProgressHook做了什么

updateWorkInProgressHook大致上做了以下几件事情:

  1. 返回了当前执行的hook的带更新队列和当前的值1
  2. 给当前的hook(currentHook)进行赋值和迭代,通过next属性

代码分析

先看看整体的代码,之后我们单个的分析执行的流程。

// 整体代码
function updateWorkInProgressHook() {
  var nextCurrentHook;
  // 第一次走到这里
  if (currentHook === null) {
    var current = currentlyRenderingFiber$1.alternate;

    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  var nextWorkInProgressHook;

  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    currentHook = nextCurrentHook;
    var newHook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null
    };

    if (workInProgressHook === null) {
      currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}
执行第一个useState

主要是通过2种情况,当刚开始执行第一个useState的时候currentHookworkInProgressHook都为null,我们需要给它赋值。所有会走到currentHook===null分支,获取到缓存树的fiber,将nextCurrentHook的值赋值为memoizedState的值。

此时nextCurrentHook的结构如下。我们知道当前的执行hook,以及下一个hook。

let nextCurrentHook = {  
   baseQueue: null,
   baseState: 0,  // 上一次的值
   memoizedState: 0, // 上一次的值
   next: {
       baseQueue: null,
       baseState"yx",
       memoizedState"yx",
       next: null, //最后一个钩子
       queue: { // 更新队列
           pending: null,
           ...
       }
   },
   queue: {
       lastRenderedState: 0,
       lastRenderedReducer: basicStateReducer,
       dispatch: func,
       pending: { // 待更新的队列 
         action: ƒ (count),
         eagerReducer: ƒ basicStateReducer(state, action),
         eagerState: 1,  // 新的值
         lane: 1, // 更新优先级
         next: {
             action: ƒ (count),
             eagerReducer: ƒ basicStateReducer(state, action),
             eagerState: 1,
             lane: 1,
             next: ....   //重复
          }
       }
     }
   }

得到nextCurrentHook的值后,继续执行,我们需要赋值currentHook的值,以及构建一个新的newHook值并返回。

function updateWorkInProgressHook() {
  var nextCurrentHook;
  if (currentHook === null) {
    var current = currentlyRenderingFiber$1.alternate;
    nextCurrentHook = current.memoizedState;
  }
  // 此时workInProgressHook也为null
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState; (也为null)
  }
  // 这里给currentHook进行赋值,这样下次执行的时候就有值了。
  currentHook = nextCurrentHook;
  var newHook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue, // 保留对应hook的更新队列
      next: null // 清空next,只关注单个hook的
  };
  // 这里给workInProgressHook进行复制操作,由于上一步它为空值
  if (workInProgressHook === null) {
      currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
  }
  return workInProgressHook
}

到这里我们就返回了第一个hook执行updateWorkInProgressHook的情况。

总结:就是根据缓存fiber获取memoizedState属性。将其赋值给nextCurrentHookcurrentHook。此时currentHookworkInProgressHook的值为如下:

currentHook = {  
   baseQueue: null,
   baseState: 0,  // 上一次的值
   memoizedState: 0, // 上一次的值
   next: {
       baseQueue: null,
       baseState"yx",
       memoizedState"yx",
       next: null, //最后一个钩子
       queue: { // 更新队列
           pending: null,
           ...
       }
   },
   queue: {
       lastRenderedState: 0,
       lastRenderedReducer: basicStateReducer,
       dispatch: func,
       pending: { // 待更新的队列 
         action: ƒ (count),
         eagerReducer: ƒ basicStateReducer(state, action),
         eagerState: 1,  // 新的值
         lane: 1, // 更新优先级
         next: {
             action: ƒ (count),
             eagerReducer: ƒ basicStateReducer(state, action),
             eagerState: 1,
             lane: 1,
             next: ....   //重复
          }
      }
   }
}
   
// 其实就是将currentHook.next 重置为null,赋值给workInProgressHook,返回给updateReducer中使用   
workInProgressHook = {
   baseQueue: null,
   baseState: 0,  // 上一次的值
   memoizedState: 0, // 上一次的值
   nextnullqueue: {
       lastRenderedState: 0,
       lastRenderedReducer: basicStateReducer,
       dispatch: func,
       pending: { // 待更新的队列 
         action: ƒ (count),
         eagerReducer: ƒ basicStateReducer(state, action),
         eagerState: 1,  // 新的值
         lane: 1, // 更新优先级
         next: {
             action: ƒ (count),
             eagerReducer: ƒ basicStateReducer(state, action),
             eagerState: 1,
             lane: 1,
             next: ....   //重复
          }
      }
   }
}
执行第二个useState

由于currentHookworkInProgressHook在执行第一个hook的时候已经被赋值。所以会走到和执行第一个hook不同的分支。在第一步中,workInProgressHook被赋值为newHook被清空了next, 所以现在nextWorkInProgressHook的值也为null

function updateWorkInProgressHook() {

    var nextCurrentHook;
    nextCurrentHook = currentHook.next; // 这就获取了name的那个hook 
    var nextWorkInProgressHook
    nextWorkInProgressHook = workInProgressHook.next; // null
    
    
    currentHook = nextCurrentHook;
    var newHook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null
    };
    // 这一步主要是给上面的nextWorkInProgressHook和workInProgressHook都赋值为newHook,
    // 因为之前nextWorkInProgressHook的值为null 
    workInProgressHook = workInProgressHook.next = newHook;
     
    return workInProgressHook
}

此时的currentHookworkInProgressHook的结构为下面的格式:

currentHook = {
  baseQueuenull
  baseState"yx"
  memoizedState"yx"
  nextnull
  queue: {
      dispatch: func
      lastRenderedReducer: basicStateReducer
      lastRenderedState"yx"
      pendingnull
  }
}

workInProgressHook =   {
      memoizedState: "yx",
      baseState: "yx",
      baseQueue: null,
      queue: {
          dispatch: func
          lastRenderedReducer: basicStateReducer
          lastRenderedState"yx"
          pendingnull
      },
      next: null
};

到这里updateWorkInProgressHook就分析完成了。在dubugger的过程中,我发现:

currentHook在每次的渲染之后,都会被renderWithHooks重置为null, 这样就能保证每一次的执行都是同样的条件。

参考链接

  1. 带你深入了解 useState