2

注:本文使用的版本是React-17.0.0.3,开启enableNewReconciler = true,即会使用new.js后缀的文件;
文章专注于hook原理和react的渲染部分,涉及到classComponent的部分一律略过;
mode = concurrent

本文所有代码都是基于以下react代码

import { React } from "../CONST";
import { useEffect, useState } from "../react/packages/react";

function FunctionComponent({name}) {
  const [count, setCount] = useState(0);
  const [subtraction, setSubTraction] = useState(1);
  useEffect(() => {
    setSubTraction(10);
  }, []);
  return (
    <div className="function border">
      {name}  {count}  {count1}
      <button onClick={() => setCount(pre => pre + 1)}>click</button>
    </div>
  );
}

const jsx = (
  <div className="box border">
    <p>start debugger</p>
    <FunctionComponent name="函数组件" />
  </div>
);

ReactDOM.createRoot(
  document.getElementById('root')
).render(jsx);

debugger源码方法
https://github.com/bubucuo/De...

制定一个小目标
通过这次源码阅读,搞清楚以下几个问题

  1. hook是怎么和当前组件关联起来的?又是如何起作用的?
  2. 为什么定义的state只会执行一次
  3. hook的异步更新怎么做的
  4. 为什么不能写在if表格块里
  5. diff算法怎么做的
  6. 在同一个事件里多次调用hook的set方法,会重渲染多次吗

自顶向下介绍React

关于concurrent模式和Fiber架构等知识请看《React技术揭秘》

fiber的结构

function FiberNode(
  tag: WorkTag, // fiberTag
  pendingProps: mixed, // 组件参数/属性
  key: null | string, // key
  mode: TypeOfMode, // 指示是lagecy还是conCurrent模式
) {
  // Instance
  this.tag = tag; // fiber类型
  this.key = key; // 用来做diff算法
  this.elementType = null;
  this.type = null;
  this.stateNode = null; // 该fiber关联的真实dom

  // Fiber
  this.return = null; // 父fiber
  this.child = null; // 第一个孩子fiber
  this.sibling = null; // 相邻的第一个兄弟fiber
  this.index = 0;

  this.ref = null;

  this.pendingProps = pendingProps; // 新的组件参数/属性
  this.memoizedProps = null; // 已经渲染在页面上的(旧的)组件参数/属性
  this.updateQueue = null; // 一个环状链表,存储的是effect副作用形成的update对象
  this.memoizedState = null; // 对于functionComponent来说,存储的是hook对象链表
  this.dependencies = null;

  this.mode = mode;

  // Effects
  this.flags = NoFlags; // 本fiber effect形成的副作用
  this.subtreeFlags = NoFlags; // 子fiber effect的副作用
  this.deletions = null;

  this.lanes = NoLanes; //本fiber的update lanes
  this.childLanes = NoLanes; // 所有子fiber的update lanes

  this.alternate = null;
  。。。
}

fibertag用来标示该fiber是哪种组件类型的fiber--25种

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
....

fiber树和dom的对比
dom树
fiber树

fiber树的生成过程

我们从render开始看fiber树是如何一个一个生成的

export function createRoot(
  container: Container,
  options?: RootOptions,
): RootType {
  return new ReactDOMRoot(container, options);
}

function ReactDOMRoot(container: Container, options: void | RootOptions) {
  this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
}

function createRootImpl(
  container: Container,
  tag: RootTag,
  options: void | RootOptions,
) {
  ...
  // 生成root
  const root = createContainer(
    container,
    tag,
    hydrate,
    hydrationCallbacks,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
  );
  ...
  return root;
}

ReactDOMRoot.prototype.render = ReactDOMLegacyRoot.prototype.render = function(
  children: ReactNodeList,
): void {
  const root = this._internalRoot;
  ...
  updateContainer(children, root, null, null);
};

ReactDOM.createRoot(document.getElementById('root'))会创建ReactDomRoot对象,该类会调用createRootImpl初始化fiberRoot对象。随后的render函数是挂在类原型上的,会调用updateContainer

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  const current = container.current;
  const eventTime = requestEventTime();

  const lane = requestUpdateLane(current);

  const update = createUpdate(eventTime, lane);
  // Caution: React DevTools currently depends on this property
  // being called "element".
  update.payload = {element};

  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    update.callback = callback;
  }

  enqueueUpdate(current, update, lane);
  const root = scheduleUpdateOnFiber(current, lane, eventTime);

  return lane;
}

显然该函数是为了给fiberRoot建立一个update,并且update.payload是element即jsx;enqueueUpdate是为了把update挂到fiber.updateQueue;最后调用了scheduleUpdateOnFiber
重点来了,scheduleUpdateOnFiber是调度函数的入口函数,从这里开始进行fiber树的构造以及update的处理;

export function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
): FiberRoot | null {
  // 从当前fiber开始向上冒泡直到找到root节点,同时更新所有父节点的childLanes
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (root === null) {
    return null;
  }

  // 标记root有一个待更新
  markRootUpdated(root, lane, eventTime);

  if (lane === SyncLane) {
    if (
      // Check if we're inside unbatchedUpdates
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      // Check if we're not already rendering
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root, eventTime);
    }
  } else {
    // Schedule other updates after in case the callback is sync.
    ensureRootIsScheduled(root, eventTime);
  }

  return root;
}

ensureRootIsScheduled函数是为了向调度堆里push一个回调函数,最后还是会调用performSyncWorkOnFiber/performConcurrentWorkOnFiber,这两个函数会调用renderRootConcurrent或者renderRootSync,进而调用workLoopConcurrent或者workLoopSync

function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
// workInProgress是个全局变量,表明当前正在处理的fiber节点,在调用后面的函数时会重新赋值
// 这两个函数唯一的不同点在与concurrent会判断shouldYield()即判断浏览器的这一帧里是否还有剩余的时间,如果时间不够的话,会中断当前执行的过程,直接进行页面渲染,防止页面卡顿

performUnitOfWork函数非常重要,主要有两点1、调用beginWork生成fiber(子fiber或者兄弟fiber),2、调用completeUnitOfWork(会调用dom方法)生成真实dom,并且挂在fiber的stateNode属性上

function performUnitOfWork(unitOfWork: Fiber): void {
  // The current, flushed, state of this fiber is the alternate. Ideally
  // nothing should rely on this, but relying on it here means that we don't
  // need an additional field on the work in progress.
  // alternate连接的是当前页面已经渲染好的fiber节点
  const current = unitOfWork.alternate;

  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }

  // 在这里用新的props覆盖旧的props
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
  
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }

  ReactCurrentOwner.current = null;
}

beginWork会根据当前fiber生成所有的子fiber(不只第一个子fiber),然后把第一个子fiber作为返回值;如果没有子fiber,返回null,进入completeunitOfWork函数

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  debugger
  let updateLanes = workInProgress.lanes;

  if (current !== null) {
    ...
  } else {
    didReceiveUpdate = false;
  }

  workInProgress.lanes = NoLanes;

  switch (workInProgress.tag) {
    // 还不清楚是classComponent还是functionComponent
    case IndeterminateComponent: {
      return mountIndeterminateComponent(
        current,
        workInProgress,
        workInProgress.type,
        renderLanes,
      );
    }
    case FunctionComponent: {
      debugger
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText:
      return updateHostText(current, workInProgress);
    ...
  }

}

beginWork里首先会判断是否可以使用上次渲染的fiber节点,didReceiveUpdate = false表明可以复用;然后会根据fiber.tag调用不同的update方法生成子fiber,我们重点关注三个:HostRoot、HostComponent和FunctionComponent
HostRoot即根节点会使用上面提到的update.payload即<div class="box border">生成子fiber,然后将此fiber返回给上层performUnitOfWork,把该fiber赋值给workInProgress,从而可以开始下次循环;由于div.box border节点有三个子节点,所以会同时生成三个子fiber,并且返回第一个子fiber<p>;<p>fiber节点只有一个文本子节点,为了节省内存,不必为该文本节点创建新的子fiber;所以p节点是没有子节点的,只能返回null,那么在performUnitOfWork函数里会进入completeUnitOfWork,完成fiber到真实dom的映射过程,完成后,如果该节点有兄弟节点,将兄弟节点赋值给workInProgress,回到performUnitOfWork函数里,如果没有兄弟节点,只能循环迭代其(祖)父节点链,将(祖)父节点赋值给workInProgress,调用completeWork,直到某个(祖)父节点有兄弟节点或者到达根节点;
下面来将整个构建流程过一遍:

渲染阶段
完成fiber树的构造或者超出浏览器一帧时间后,调用commitRoot->commitRootImpl完成dom节点挂到root节点上的工作;

hook-useState、useEffect

好了fiber树的构建以及react渲染流程已经简单的过了一遍,那么hook在哪里呢?
还记得在构建fiber树的时候有一个节点是functionComponent吧,在beginWork函数里会命中FunctionComponent或者IndeterminateComponent,这两个函数都会调用renderWithHooks,然后会使用调用Component(fiber.type)方法,对于functionComponent组件来说,fiber.type就是组件函数本身,执行该函数,也会执行组件里使用的hook

function FunctionComponent({
  name
}) {
  _s();

  const [count, setCount] = Object(_react_packages_react__WEBPACK_IMPORTED_MODULE_2__["useState"])(0);
  const [subtraction, setSubtraction] = Object(_react_packages_react__WEBPACK_IMPORTED_MODULE_2__["useState"])(1);
  Object(_react_packages_react__WEBPACK_IMPORTED_MODULE_2__["useEffect"])(() => {
    setCount1(10);
  }, []);
  return /*#__PURE__*/Object(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__["jsxDEV"])("div", {
    className: "function border",
    children: [name, "  ", count, "  ", subtraction, /*#__PURE__*/Object(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__["jsxDEV"])("button", {
      onClick: () => setCount(pre => pre + 1),
      children: "click"
    }, void 0, false, {
      fileName: _jsxFileName,
      lineNumber: 19,
      columnNumber: 7
    }, this)]
  }, void 0, true, {
    fileName: _jsxFileName,
    lineNumber: 17,
    columnNumber: 5
  }, this);
}

以上就是本例中使用到的函数组件的jsx编译后的样子,显然在执行该函数的时候也会执行里面写的useState和useEffect方法;

const HooksDispatcherOnMount: Dispatcher = {
  ...
  useEffect: mountEffect,
  useState: mountState,
  ...
};

const HooksDispatcherOnUpdate: Dispatcher = {
  ...
  useEffect: updateEffect,
  useState: updateState,
  ...
};

可以看到,hook的实现有两套(实际上不止两套),分别对应mount和update阶段;那么是在什么时候切换的呢?

export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {

  if (__DEV__) {
    
  } else {
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }
  ...
  return children;
}

上文我们讲到functionComponent会调用renderWithHooks,所以就是在这个函数里会根据当前是mount阶段还是update阶段进行切换;
下面我们来看下hook具体是怎么实现的。因为useState和useEffect实现有很大的不同,所以我们分开来讲;

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 生成hook,并且挂在workInProgressHook和当前fiber即urrentlyRenderingFiber的memoizedState上
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    // 挂载阶段会执行该函数
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null, // update链
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });

  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}
// 生成hook,并且挂到fiber.memoizedState上(functionComponent的memoizedState才会挂hook链表),更新workInProgressHook
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null, // 在页面渲染的值

    baseState: null, // state计算的中间值
    baseQueue: null, // 上次遗留的queue
    queue: null,

    next: null, // 下一个hook
  };

  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;
}


function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {

  const eventTime = requestEventTime();
  const lane = requestUpdateLane(fiber);

  const update: Update<S, A> = {
    lane,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };
  const alternate = fiber.alternate;
  ...
  const pending = queue.pending;
  if (pending === null) {
    // This is the first update. Create a circular list.
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
  const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
  ...
}

对于useState,如果参数是个函数,就会去执行该函数,如果不是函数,直接使用该值,赋值给hook.memoizedState;新建的queue主要是时用来存储更新链表的,在pending后会接一个环状链表,dispatch就是我们调用的更新函数;看dispatchAction,每次调用该hook,都会生成一个update(在dispatch里),并且将该update插入到环状链表的第一个(即pending直接指向该update);
好了,现在我们有了hook的值以及它的更新函数,如果我点击button,调用了setCount,给该hook.queue.pending上插入了一个update,那么它是什么时候计算呢?
在更新阶段时调用的useState
没错,当我们非首次渲染functionComponent需要更新时,会重新执行组件函数,也就会在此调用useState,只不过这一次在源码里调用的是updateState

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  // $FlowFixMe: Flow doesn't like mixed types
  return typeof action === 'function' ? action(state) : action;
}

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  queue.lastRenderedReducer = reducer;

  const current: Hook = (currentHook: any);

  // The last rebase update that is NOT part of the base state.
  let baseQueue = current.baseQueue;

  // The last pending update that hasn't been processed yet.
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    // We have new updates that haven't been processed yet.
    // We'll add them to the base queue.
    // 有新的update待处理,把他们加到base queue里
    if (baseQueue !== null) {
      // Merge the pending queue and the base queue.
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }

    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  if (baseQueue !== null) {
    // We have a queue to process.
    const first = baseQueue.next;
    let newState = current.baseState;

    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
      const updateLane = update.lane;
      // 重!!!lane结构的体现
      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // Priority is insufficient. Skip this update. If this is the first
        // skipped update, the previous update/state is the new base
        // update/state.
        // 优先级不够,跳过这个update,如果这是第一个跳过的update,早前的update/state就是最新的基本update/state
        const clone: Update<S, A> = {
          lane: updateLane,
          action: update.action,
          eagerReducer: update.eagerReducer,
          eagerState: update.eagerState,
          next: (null: any),
        };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        // Update the remaining priority in the queue.
        // TODO: Don't need to accumulate this. Instead, we can remove
        // renderLanes from the original lanes.
        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane,
        );
        // 标记跳过update的lane
        markSkippedUpdateLanes(updateLane);
      } else {

        // Process this update.
        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: any): S);
        } else {
          const action = update.action;
          // 异步更新的奥秘,action是function,会把新算的newState作为参数执行,但是action是值的化,直接返回值,然鹅在同一次render里,memoizedState是同一个值
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      // 又是一个环
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }

    // Mark that the fiber performed work, but only if the new state is
    // different from the current state.
    if (!is(newState, hook.memoizedState)) {
      // 可以复用以前的update--还没看懂所有链路
      markWorkInProgressReceivedUpdate();
    }

    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    // baseQueue是以前没更新过的updatelist,比如被sikp的
    hook.baseQueue = newBaseQueueLast;

    queue.lastRenderedState = newState;
  }


  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

可以看到updateState方法是updateReducer的一层封装,reducer参数使用的是一个全局函数;在updateReducer函数里有这一行newState = reducer(newState, action),这行代码就是为什么我门调用set函数时只有传入函数参数才能解决其异步的问题的原因。

在同一个事件里多次调用set
我们修改jsx代码

function FunctionComponent({name}) {
  const [count, setCount] = useState(0);
  const [count1, setCount1] = useState(1);
  useEffect(() => {
    setCount1(10);
  }, []);
  return (
    <div className="function border">
      {name}  {count}  {count1}
      <button onClick={() => {
        setCount(pre => pre + 1);
        setCount(pre => pre + 1);
        setCount(pre => pre + 1)
        setCount(pre => pre + 1)
      }}>click</button>
    </div>
  );
}

在点击button时,调用了四次setCount,会触发四次dispatchAction函数,每次函数执行都会向hook的queue.pending上插入一个update。然后调用scheduleUpdateOnFiber函数,触发ensureRootIsScheduled,构造一个回调函数,由于我们的环境时支持微任务的,所以会调用scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root))把performSyncWorkOnRoot放到syncQueue任务队列里,接着调用scheduleMicrotask(flushSyncCallbacks);在微任务执行时机执行flushSyncCallbacks,在这个函数里会从syncQueue里依次取出函数执行(也就是performSyncWorkOnRoot);由于微任务调用时四次调用dispatchAction已经完成,所以该hook上已经挂上了四个update,这样在重新构建fiber树时就会执行这些update,完成更新;

大家可能要问了,一共四次调用,是不是会在syncQueue里推入四个函数,然后微任务执行阶段执行四次performSycnWorkOnRoot呢?

这当然是不可能的,在ensureRootIsScheduled函数里会比较root.callbackPriority和当前更新的newCallbackPriority(具体怎么得来的以后再说),如果是一样的话,就直接return,不再执行下面的内容。由于第一次调用ensureRootIsScheduled的最后会把newCallbackPriority挂到root.callbackPriority上,而这四次调用产生的newCallbackPriority是一样的,所以后面三次调用都没能得到向syncQueue里push的机会。


奈何点点寒
6 声望1 粉丝