web前端技术分享
useState_useState函数调用栈

数据更新->视图更新,是现代前端框架的根本目的。原生js和jquery在项目开发中,代码逻辑中穿插大量dom操作,会给项目的开发,维护,稳定运行带来很大的困境。所以在我看来,现代前端框架所要解决的核心问题,就是数据和视图分离(代码逻辑中的分离),或者说是数据和视图的绑定(所得既所见),从前我们处理完数据需要手动去更新dom操作,而有了前端框架,我们便可以-所得既所见,当我们根据业务逻辑更改了数据,不需要我们手动操作dom节点,这些现代前端框架会去帮我们完成刷新,所以在我看来,现代前端框架得核心功能只有一个,就是那加粗的五个字,**所得既所见。**至于其它的,什么生命周期,副作用,状态管理, ref等等,或是什么fiber架构,diff算法,concurrent模式,状态队列等等,都只是为了完成这个目标,或者更好的完成这个目标。在这一点上,react也好, vue也好,都是如此。只不过两者在具体实现上采用的策略存在差异,如数据更新触发视图更新,vue采用getter, setter侦听来实现数据到视图的双向绑定,自动更新,而react则通过手动调用setState来触发视图更新,所得既所见的目的相同,只是实现策略不同。 综上所述,数据更新->试图更新,所得既所见。就是我所认为的现代前端框架的核心,而其它的一切皆围绕于此。所以关于react的故事,我想必须从useState开始。

一、在使用是我们从react中导出useState使用,所以找到源码/packages/react/src/react.js为使用该库的入口文件。

找到导出的setState,看他是从哪里引入的,你会发现导出的setState来自源码/packages/react/src/reactHook.js image.png resolveDispatcher实现如下: image.png 结合这两段代码当我们在项目中使用useState时:const [state, setState] = useState(initialState); 就相当于 const [state, setState] = ReactCurrentDispatcher.current.useState(initialState)。

二、ReactCurrentDispatcher又是什么?

变量定义在/packages/react/src/ReactCurrentDispatcher.js,代码如下。 image.png 如你所见,整个源文件只有几行,这里只进行了变量定义。ReactCurrentDispatcher的current属性又是在哪里赋值的呢?直接讲答案:packages\react-reconciler\src\ReactFiberHooks.js。 ReactCurrentDispatcher在/packages/react/src/ReactSharedInternals.js共享给其它文件夹。源码的/packages/shared/ReactSharedInternals.js收集这些全局共享变量。这些全局共享变量可以通过/packages/shared/ReactSharedInternals.js引入,在源码全局范围内修改或使用。

三、所以讲视线转到packages\react-reconciler\src\ReactFiberHooks.js中,看ReactCurrentDispatcher.current是如何赋值的。

在ReactFiberHooks.js源码中找到renderWithHooks函数,你可看到如下代码: image.png 这里你可以看到ReactCurrentDispatcher.current是如何赋值的。外层分支区分生产开发环境,内层分支主要分两种情况更新Update,挂在Mount。会根据renderWithHooks的调用时机选择不同的赋值。我们先以挂载mount为例,看看HooksDispatcherOnMount,到底是个什么。 image.png 这个对象便是ReactCurrentDispatcher.current在挂载阶段的值了。那么它的useState就对应mountState变量。 所以(一)中所讲:”在项目中使用useState时:const [state, setState] = useState(initialState); 就相当于 const [state, setState] = ReactCurrentDispatcher.current.useState(initialState)。“ 在这里,又等价于const [state, setState] = mountState(initialState);

四、我们继续看mountState是什么?

mountState的实现仍位于packages\react-reconciler\src\ReactFiberHooks.js中。 image.png 如(三)中所讲:我们在项目中使用useState时const [state, setState] = useState(initialState); 等价于const [state, setState] = mountState(initialState); 那其实根据返回值你就可以知道,你的state和setState其实就分别对应mountState中的hook.memoizedState, dispatch。 所以当我们调用setState时,就是调用的mountState方法中的dispatch变量方法。 那么根据截图中的1751代码行,dispatch方法就是经过bind的dispatchSetState方法。

五、dispatchSetState

dispatchSetState的实现依旧在packages\react-reconciler\src\ReactFiberHooks.js中。从函数名你也可以看出,dispatchSetState:派发setState。源码如下:

  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 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
): void {
  if (__DEV__) {
    if (typeof arguments[3] === 'function') {
      console.error(
        "State updates from the useState() and useReducer() Hooks don't support the " +
          'second callback argument. To execute a side effect after ' +
          'rendering, declare it in the component body with useEffect().',
      );
    }
  }

  const lane = requestUpdateLane(fiber);

  const update: Update<S, A> = {
    lane,
    revertLane: NoLane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    const alternate = fiber.alternate;
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // The queue is currently empty, which means we can eagerly compute the
      // next state before entering the render phase. If the new state is the
      // same as the current state, we may be able to bail out entirely.
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        if (__DEV__) {
          prevDispatcher = ReactCurrentDispatcher.current;
          ReactCurrentDispatcher.current =
            InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          // Stash the eagerly computed state, and the reducer used to compute
          // it, on the update object. If the reducer hasn't changed by the
          // time we enter the render phase, then the eager state can be used
          // without calling the reducer again.
          update.hasEagerState = true;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            // Fast path. We can bail out without scheduling React to re-render.
            // It's still possible that we'll need to rebase this update later,
            // if the component re-renders for a different reason and by that
            // time the reducer has changed.
            // TODO: Do we still need to entangle transitions in this case?
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
            return;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          if (__DEV__) {
            ReactCurrentDispatcher.current = prevDispatcher;
          }
        }
      }
    }

    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      scheduleUpdateOnFiber(root, fiber, lane);
      entangleTransitionUpdate(root, queue, lane);
    }
  }

  markUpdateInDevTools(fiber, lane, action);
}

关注我粘贴的这段代码的74~78行。 image.png 最重要的是第76行: image.png 当我们在项目中执行setState时其实就是执行的上述代码,而关键方法就是这个scheduleUpdateOnFiber。

六、scheduleUpdateOnFiber

沿着scheduleUpdateOnFiber->ensureRootIsScheduled->scheduleTaskForRootDuringMicrotask的调用链 在scheduleTaskForRootDuringMicrotask方法的源码实现中,你可以找到如下代码: image.png 该段代码的含义为创建新任务(performConcurrentWorkOnRoot)将其加入调度器的调度队列中(scheduleCallback()),performConcurrentWorkOnRoot为以Concurrent模式启动渲染fiber的方法。至此setState方法就完成了将任务:以Concurrent模式启动渲染fiber,添加到调度器调度队列中。调度器会在合适的时机调度该任务执行。

那么这里就引出了两个概念,调度器和fiber。调度器是指渲染任务的调度机制,fiber你可以认为是虚拟Dom。这两个概念都是一套完整的机制,之后都会细说。