面试官所不知道的《React源码v18》(2w字+)(持续更新)

1,566

看了网上大部分源码材料,今天笔者决定解析React源码,想说一下的是React作为大型前端框架,想要理解内部思想必然是困难的,主要是大部分内容存在耦合,充斥大量关联性代码,笔者并不觉得自己的文章一定正确,所以希望您和笔者一起撕源码,指出不足,冲!(最后笔者表示,该文章会不断更新,直到笔者认为看完全文后您对源码的理解已经成为您的一项杀手锏或者说对于大部分前端er是不存在能考倒您的能力的)「看完文章后,如果对您有帮助,希望您给个“赞”鼓励一下」,如果没有,您的本次阅读也是笔者继续更新的动力」

React整体架构

  • React宏观采取心智模式进行渲染更新,类似加工流程,对每一次任务分配lane表示当前任务的优先级,因此对于任务也会根据优先级区别对待,并且任务采取可插队策略,当遇到当前紧急任务时,会退出当前任务循环,进入紧急任务.
  • 一次任务的调度过程可以以
    • Schedule(安排更新优先级,进行渲染)

    • render(根据当前Fiber树(current)渲染新的Fiber树(workInProgress))

    • commit(进行替换current,依据Fiber树的区别生成或更新dom,并执行副作用).

Schedule(调度器)

  • 作用: 决定当前执行任务的阶段,React细分每次任务调度为一帧(60HZ,16.6ms),为了给渲染线程流出时间更新,js有5ms的执行时间,如果超出那么会打断当前任务.这种方案被称为时间分片.
  • 问题与方案:
    • 问题: React为了避免掉帧会为js执行留下5ms的任务调度时间,如果超时,会停止当前任务,将线程执行权交给渲染线程,那么就需要保存这次未完成的更新,并在合适的时机去执行.

    • 方案: 因此每次打断当前调度时,都需要创建一个新的任务,该任务在宿主环境存在MessageChannel方法时,会调用该方法,如果不存在,会退而求其次选择setTimeout.

      • 1、该创建的任务必须为宏任务,如果是微任务那么依然会阻塞本次任务循环.
      • 2、如果采用setTimeout,我们以chorme为例,当嵌套调用setTimeOut时,为了保证让出render的时间,会有4ms的延迟锁死时间,这个时间是React不想浪费的,所以它并不是最优解.
      • 3、rAF,在该次渲染之前检查执行,如果渲染存在延迟,会导致等待,造成性能损耗.

React下的代数效应

  • React极大的表达了纯函数的概念,将副作用抽离到函数之外,从而保持函数的纯洁性,使输入和输出一一对应,hook和redux就很好的体现了这一点,使用户关注只在函数作用本身.
  • 其实浏览器原生就存在打断任务调度的方式(Genatator).那不采取的原因是什么呢?
    • React团队的sebmarkbage在react的issues提到过这一点,React不使用Genaragtor的原因是在生成器的内部所存在的函数会影响堆栈,并增加语法开销 同时生成器的有状态导致了无法从中间恢复, 例如你顺序执行doWork(A)、doWork(B)、doWork(C),当你继续执行更新B「B依赖A的值」,此时,你所依赖的A必定是上次计算的,而你可能希望A也更新,但生成器不允许你这么做.
  • React建立了一套独特的Fiber架构,来处理任务中断与恢复、优先级调度等问题.

Fiber架构

  • 策略: React在调度阶段采取了双缓冲策略,解决由于渲染线程和V8线程互斥引起的掉帧问题
    • 双缓存技术: 为了解释这点,我们以打水为例,当我们在取水时,如果我们只用一个水桶,那我们线性的工作流程必然是缓慢执行的,那么水龙头必然会有停滞的时间(等待调度),但是当我们用两只水桶,取水和倒水并发执行时,那么就解决了水龙头停滞的问题,极大的提升了效率,chorme浏览器的html绘制实际也是采取的这种策略,React为了提升重绘效率也是采取了这种方案.
    • React的双缓冲即在内存中维护一颗FiberTree,该FiberTree被currentFiberTree通过alternate指向,这两棵Tree分别对应了屏幕中的DomTree在内存中由于副作用或mount形成的可以转换为DomTree的FiberTree.

React.Render

「beginWork」

该过程主要是“递”的过程,在该过程中,我们主要是通过深度优先遍历,对Fiber节点进行执行或(标记和重用处理(Diff算法)),从而创建一颗与current Fiber关联的workInprogress Fiber树.

  • render的过程主要是
    • 1、创建一颗基于当前Fiber结构的workInprogressTree.
    • 2、和上一次更新的FiberTree进行diff比较,对节点进行标记,并在compelete阶段对Fiber标记映射dom操作.
    // 我们以FunctionComponent为例.
    nextChildren = renderWithHooks( // 执行函数组件,返回当前Fiber下的第一个子Fiber
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderLanes,
    );
    // diff.将当前workInprogressFiber和currentFiber进行比较,进行标记
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
    return workInProgress.child;
    

renderWithHook

  • 执行FunctionComponent并设置组件的副作用循环链表,hook链表并返回Fiber.children,用于深度优先.
  • 通过下面的函数我们来看几点.(划线可以暂时不了解)
    • 1、我们设置了全局的渲染优先级,该优先级的作用就是在执行Component时,对于高于或等于本次渲染优先级的副作用操作,优先执行
    • 2、设置全局renderFiber、hooks链表、副作用队列、Fiber所含车道「该车道存储当前Fiber下的所有更新车道」
    • 3、判断在执行FunctionComponent时是否存在hook操作,如果存在,循环执行.
    • 4、判断currentFiber的hook链表,对于两次渲染的hook链表长度不一致的情况,throwError.
export function renderWithHooks<Props, SecondArg>(...): any {
  // 本次渲染优先级
  renderLanes = nextRenderLanes;
  // 正在渲染hook, memoizeState存储hook链表
  currentlyRenderingFiber = workInProgress;
  // 保存hooks链表
  workInProgress.memoizedState = null;
  // 保存副作用队列
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;
  // 执行FUnction函数
  let children = Component(props, secondArg);
  // 判断render阶段的update(dispatch操作等) 🌹
  // 对于在render阶段进行的dispatch会循环执行
  // Check if there was a render phase update
  if (didScheduleRenderPhaseUpdateDuringThisPass) {
    // 记录重绘次数
    let numberOfReRenders: number = 0;
    do {
      didScheduleRenderPhaseUpdateDuringThisPass = false;
      localIdCounter = 0;
      if (numberOfReRenders >= RE_RENDER_LIMIT) {
        throw new Error(...);
       }
      numberOfReRenders += 1;
      currentHook = null;
      workInProgressHook = null;
      workInProgress.updateQueue = null;
      ReactCurrentDispatcher.current = __DEV__
        ? HooksDispatcherOnRerenderInDEV
        : HooksDispatcherOnRerender;

      children = Component(props, secondArg);
    } while (didScheduleRenderPhaseUpdateDuringThisPass);
  }
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;
  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);
  currentHook = null;
  workInProgressHook = null;
  didScheduleRenderPhaseUpdate = false;
  return children;
}
  • 对于classComponent,会执行updateClassComponent,在本函数中我们会执行updateClassInstance系列函数(需要深入),在该函数中,会调度processUpdate,它会解开本次渲染中的通过setState将本次更新放入到Fiber.updateQueue.sharing.pending形成循环链表,和上次更新的lastUpdate形成单链表(Fiber.effects),从而顺序执行获取newState

--Diff算法--

「由于$$typeof类型众多,我们仅考虑REACT_ELEMENT_TYPE」

场景: 该过程发生在beginWork阶段, 在该阶段会进行新旧Fiber的对比及更新

单节点比较
  • 这里使用Fiber和newChild进行比较协调,因为我们现在需要创建的新Fiber树(woekInProgress Fiber树),是需要通过JSX创建的ReactElement对象以及上次更新的Fiber节点来获得的.
    • case REACT_ELEMENT_TYPE: // 入口函数
       return placeSingleChild(
         reconcileSingleElement(
           returnFiber, // 表示returnFiber节点,也就是父Fiber
           currentFirstChild, // 上一次更新Fiber,通过sibling形成单链表
           newChild, // ReactElement
           lanes, // 车道「这一节我们不考虑」
         ),
       );
      
    • reconcileSingleElement函数是通过遍历currentFirstFiber链表来决定是否存在可利用的节点,请看代码「省去了与Diff无关的代码」
    function reconcileSingleElement(
        returnFiber: Fiber,
        currentFirstChild: Fiber | null,
        element: ReactElement,
        lanes: Lanes,
    ): Fiber {
    const key = element.key;
    let child = currentFirstChild; // 上一次更新的头节点
    // 遍历
    while (child !== null) {
    // 判断key值
      if (child.key === key) {
        const elementType = element.type;
        // 判断元素标签
        if (child.elementType === elementType) {
           // 移除以第二个参数为首的后续链表节点
           // 在父节点对象上的deletions数组「标记需要删除的子节点数组」
           // 在子节点的flags上通过位运算标记该节点需要移除
           deleteRemainingChildren(returnFiber, child.sibling);
           // 复用节点,并传入新props
           const existing = useFiber(child, element.props);
           // 移动ref指向
           existing.ref = coerceRef(returnFiber, child, element);
           // 父节点指向.
           existing.return = returnFiber;
           return existing;
        }
      }
        // 因为key存在唯一性,如果key相等但是元素类型变了,那么后续就不需要遍历直接清空.
        deleteRemainingChildren(returnFiber, child);
        break;
    } else {
       // 移除当前节点
       deleteChild(returnFiber, child);
    }
      child = child.sibling;
    }
     // 不可复用,新建.
     const created = createFiberFromElement(element, returnFiber.mode, lanes);
     created.ref = coerceRef(returnFiber, currentFirstChild, element);
      created.return = returnFiber;
      return created;
    }
    
    • 判断新建节点,如果节点未与returneFiber连接, 标记PlaceMent(新增节点)
    • function placeSingleChild(newFiber: Fiber): Fiber {
        // This is simpler for the single child case. We only need to do a
        // placement for inserting new children.
        if (shouldTrackSideEffects && newFiber.alternate === null) {
          newFiber.flags |= Placement;
        }
        return newFiber;
      }
      
多节点比较
  • 多节点比较相较于单节点更加复杂.
  • if (isArray(newChild)) { // 判断节点数组
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }
    
  • 我们需要考虑两条链表以最少的时间复杂度,实现可复用节点的查找与拼接
  • React的Diff思路是,「实现两次遍历」
    • React的diff极其依赖于key,即便是节点类型相等,但只要key值不相等就会重建,换句话说,指要是key相等,即便元素类型不相等,也不会去后续的可复用遍历
    • 第一次按序一比一遍历,如果节点皆可复用,那么就不需要多余的操作
    • 如果旧节点复用完成而新节点依旧存在,那么证明可复用的结束,剩下全部新建.
    • 如果新节点复用完成而旧节点依然存在,那么旧节点只需要删除.
    • 如果第一次遍历中按序遍历,遇到无法复用,中断第一次遍历,进入第二次遍历,将oldFiber以Fiber.key为key(如果不存在,以Fiber.index为key),以Fiber为value,建立map.如果newChild.key可以被map索引,那么证明存在复用节点,反之,进行新建.(开始源码)
变量相关
-        // diff 算法
        // 记录经过diff之后新的Fiber链表头部
        let resultingFirstChild: Fiber | null = null;
        // 记录上一次遍历的newFiber节点
        let previousNewFiber: Fiber | null = null;

        // 记录当前的旧节点
        let oldFiber = currentFirstChild;
        // 在oldFiber中的最后一个可复用的下标
        let lastPlacedIndex = 0;
        // 记录新节点的下标
        let newIdx = 0;
        // 记录下一次比较的旧节点
        let nextOldFiber = null;
第一次遍历

第一次遍历的目的是, 对于未更改的ArrayFiber,通过O(n)直接遍历完成,不需要进行多余的操作,否则立刻break,将复杂操作交给第二次遍历(注意React在diff时极其依赖于key,如果key值一样,其实元素类型不相等,也会直接创建,而不是退出). 这里笔者发现大部分文章错误的地方「也可能是React进行优化的地方」,对于不定义key的ReactElement,React并没有create, 而是依然进行elementType、key(null)的比较,实现复用(虽然控制台会warning),当然, 笔者也通过实践确定了该观点,而这与之前笔者看到的大部分描述相悖,也充分证明了阅读源码的必要性

for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
  // 老节点的索引比新节点大了,需要插入新的节点,并且固定旧节点进行比较.
  if (oldFiber.index > newIdx) {
    nextOldFiber = oldFiber;
    oldFiber = null;
  } else {
    nextOldFiber = oldFiber.sibling;
  }
  // 进入Fiber和child比较,决定是否复用, 如果不可复用返回null
  const newFiber = updateSlot(
    returnFiber,
    oldFiber,
    newChildren[newIdx],
    lanes,
  );
  // 当前不可复用, 退出第一次遍历
  if (newFiber === null) {
    if (oldFiber === null) {
      oldFiber = nextOldFiber;
    }
    break;
  }

  // 定位最后一个可复用下标
  lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
  if (previousNewFiber === null) {
    resultingFirstChild = newFiber;
  } else {
    previousNewFiber.sibling = newFiber;
  }
  previousNewFiber = newFiber;
  oldFiber = nextOldFiber;
}
- 这里我们需要谈论一下placeChild函数,该函数是通过`比较在该次遍历以前的最后一个可复用节点的位置和当前newIdx进行比较,从而确定是否是否存在节点移动,React会通过shouldTrackSideEffects变量确定「更新」或者「新建」,如果是新建那么只会在第一个Fiber上标记Forked,而不是对每一个Fiber都标记placeMent,这么做的目的是提高性能`

我们不具体展开「全部复用」,「oldFiber全部复用剩下newChild新建」, 「newChild全部复用剩余oldFiber移除问题」情况, 这些情况是可预测的.

第二次遍历

到了这里, 表示oldFiber与newChildren都未遍历完成, 这里可预测两种情况, 1、newChildren被移动,但是依然可复用. 2、节点被插入

  •   // 建立oldFiber.key为key「null时,用index代替」,oldFiber为value的键值对.
      const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
      // 从第一次未遍历完成的下标开始.
      for (; newIdx < newChildren.length; newIdx++) {
        // 从oldFiber Map中查找newChildren是否存在,以O(1)确定节点是否被移动
        const newFiber = updateFromMap(
          existingChildren,
          returnFiber,
          newIdx,
          newChildren[newIdx],
          lanes,
        );
        if (newFiber !== null) {
        // 更新状态下
          if (shouldTrackSideEffects) {
          // 如果Fiber可重用
            if (newFiber.alternate !== null) {
            // 从键值对中移除对应的oldFiber,(该map后续有用处)
              existingChildren.delete(
                newFiber.key === null ? newIdx : newFiber.key,
              );
            }
          }
          lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
          // 链表移动
          if (previousNewFiber === null) {
            resultingFirstChild = newFiber;
          } else {
            previousNewFiber.sibling = newFiber;
          }
          previousNewFiber = newFiber;
        }
      }
    // 更新状态下, 我们需要通过oldFiber Map进行标记delete,这也是为什么之前将可复用Fiber移除Map.
      if (shouldTrackSideEffects) {
        existingChildren.forEach(child => deleteChild(returnFiber, child));
      }
    
  • 补充: 对于Diff中确定可复用节点是比较重要的点, 我们稍微展开一下.
  • 这里我们以oldFiber: A->B->C->D「箭头表示sibling指向」, newChildren: A'->C'->B'->D'(A'表示Fiber A对应的ReactElement).
    • 首先第一次遍历时,我们确定A可直接复用,B与C'不对应,且两个链表都未遍历完.此时placeIndex = 0(最后一个可复用节点下标),
    • 进入第二次遍历,建立了BCD相关的map, 从中得出C'对应C的oldFiber Index = 2, 2 > placeIndex, 我们重新定义placeIndex = 2,该节点依然不需要移动, 并且将C从oldFiber map中移除.
    • 下一个B'节点对应的Index = 1, 1 < placeIndex, 该节点需要移动, 但依然可复用,通过alterate进行相互绑定,并为B标记PlaceMent表示需要移动,placeIndex依然为2.并从map中删除B
    • 下一个D'的index = 3,3 > placeIndex,节点不需要移动,继续从map中删除D, 遍历结束.

总结: Diff的资源是比较多的,很多文章都有对它的介绍,并且它的耦合度也是较低的,是可以拿出来单独翻的一块,当然也是需要了解大致流程的,不过看再多的文章也比不过手撕,所以还是希望读者可以去翻一下,这样你可以很自信的了解Diff流程,本文对链表的操作没有着重阐述,因为默认读者是有数据结构基础的,接下来我们继续😁

「completeWork」

进入当前阶段, 我们对Fiber的处理已经完成,接下来就是进行Dom的处理. 在这里笔者暂时以HostComponent作为Tag类型进行解析. 该阶段的处理主要分为更新创建两个阶段.笼统点说该阶段就是对对应Fiber的属性进行挂载或更新(尤其是props).

主函数入口

  • // 当前workinprogress Fiber对应tag为HostComponent
    case HostComponent: {
    // 从栈中弹出当前Fiber
    popHostContext(workInProgress);
    // 拿到根节点实例
    const rootContainerInstance = getRootHostContainer();
    // type对于Function Com表示函数本身
    // 对于Class Com表示类本身
    // 对于原生标记标是标签名
    const type = workInProgress.type;
    // 已有Fiber树存在, 更新阶段
    if (current !== null && workInProgress.stateNode != null) {
    // 更新props
      updateHostComponent(
        current,
        workInProgress,
        type,
        newProps,
        rootContainerInstance,
      );
      //标记ref Flags, 表示该Fiber ref需要更新.
      if (current.ref !== workInProgress.ref) {
        markRef(workInProgress);
      }
    } else {
      // 新建Component
      if (!newProps) {
        if (workInProgress.stateNode === null) {
          throw new Error(
            'We must have new props for new mounts. This error is likely ' +
              'caused by a bug in React. Please file an issue.',
          );
        }
    
        // This can happen when we abort work.
        bubbleProperties(workInProgress);
        return null;
      }
      // 获取当前游标指向的Fiber, 指向当前Fiber的nameSpace
      const currentHostContext = getHostContext();
      // 是否被强制更新, 我们不过多分析
      const wasHydrated = popHydrationState(workInProgress);
      if (wasHydrated) {
        if (
          prepareToHydrateHostInstance(
            workInProgress,
            rootContainerInstance,
            currentHostContext,
          )
        ) {
          markUpdate(workInProgress);
        }
      } else {
        // 新建instance
        // 创建DOM实例
        const instance = createInstance(
          type,
          newProps,
          rootContainerInstance,
          currentHostContext,
          workInProgress,
        );
        // 将当前实例和所有子节点建立dom或return的绑定
        appendAllChildren(instance, workInProgress, false, false);
        // 当前Fiber指向当前instanceDOM
        workInProgress.stateNode = instance;
         
        if (
          finalizeInitialChildren(
            instance,
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
          )
        ) {
          markUpdate(workInProgress);
        }
      }
      
      if (workInProgress.ref !== null) {
        markRef(workInProgress);
      }
    }
    // 优先级设定, 将当前Fiber的车道lanes和flags向上传染.
    bubbleProperties(workInProgress);
    return null;
    }
    
创建阶段.

这里的内容和Diff是相关联的,实际进入这里的操作是由于diff中,我们的节点无法复用使得我们必须新建Fiber「stateNode为null」,然后在这里我们再新建dom,并和workInprogress.stateNode以及子节点的domTree连接

  • appendAllChildren(instance, workInProgress, false, false);
    • 该函数作用是建立当前实例和子节点的关联.并且很好的表达了的含义,希望您细品
    • appendAllChildren = function(
        parent: Instance,
        workInProgress: Fiber,
        needsVisibilityToggle: boolean,
        isHidden: boolean,
      ) {
        // 对于Fiber对应的dom元素, 进行元素挂载.
        // 对于子Fiber, 进行自当前Fiber以下的所有childFiber的return的“归”式向上绑定.向上连接
        let node = workInProgress.child;
        // 该函数循环旨在对于不同类型的Fiber,我们需要有不同的处理,如果当前Fiber并不对应原生Component或者Text,那么它并不能建立DOM之间的关系(ClassComponent等),我们需要深度优先遍历进入当前Fiber.child,找到挂载关系.并不断建立return指向「归」,当然我们还需要处理sibling的关系,那么所有的子Fiber就和当前的实例建立了appendChild.
        // ***本次循环过后, 当前Fiber对应的instance与child层的instance建立了父子关联***
        while (node !== null) {
          // 符合DOM挂载条件.
          if (node.tag === HostComponent || node.tag === HostText) {
            // 父子元素挂载
            appendInitialChild(parent, node.stateNode);
          } else if (node.tag === HostPortal) {
          } else if (node.child !== null) {
          // 这里我们继续深入child,并且continue
            node.child.return = node;
            node = node.child;
            continue;
          }
          // 归的过程已经当顶, return
          if (node === workInProgress) {
            return;
          }
          // 当前node已对应原生DOM, 深度优先结束,我们回溯查找.
          while (node.sibling === null) {
            if (node.return === null || node.return === workInProgress) {
              return;
            }
            node = node.return;
          }
          // sibling存在,我们让sibling和node.return建立父子关系
          node.sibling.return = node.return;
          node = node.sibling;
        }
      };
      
  • finalizeInitialChildren
    • export function finalizeInitialChildren(
          domElement: Instance,
          type: string,
          props: Props,
          rootContainerInstance: Container,
          hostContext: HostContext,
      ): boolean {
        // 设置属性(重点关注函数)
        setInitialProperties(domElement, type, props, rootContainerInstance);
        // 查找props的autoFocus决定是否标记update Flags.(不深究)
        return shouldAutoFocusHostComponent(type, props);
      }
      
    • setInitialProperties函数主要是将我们新的pendingProps作用到DOM上.
    •    export function setInitialProperties(
        domElement: Element,
        tag: string,
        rawProps: Object,
        rootContainerElement: Element | Document,
      ): void {
        // 我们根据标签的“-”以及props的is(动态绑定)决定是否是自定义组件
        const isCustomComponentTag = isCustomComponent(tag, rawProps);
        let props: Object;
      
        // 这个过程中我们会对部分原生组件所含有的独特Attr并没有采用RootContainer去监听,而是原始的放在了dom上.「参照React事件篇.」
        swich(tag) {
                case 'dialog':
                // 事件注册
                listenToNonDelegatedEvent('cancel', domElement);
                listenToNonDelegatedEvent('close', domElement);
                props = rawProps;
                break;
                ......
        }
      
        // 判断props是否合法, style、children和dangerous、单标签的children,不合法throw Error
        assertValidProps(tag, props);
      
        // 设置初始化属性.
        // 该过程我们会查找style列表,将它标准化,通过dom.style设置到dom上,并设置children和dangerousHTML.
        setInitialDOMProperties(
          tag,
          domElement,
          rootContainerElement,
          props,
          isCustomComponentTag,
        );
        // 这个过程, 我们主要是对拥有defaultValue,value,defaultChecked等属性标准化,并且将defaultValue的值赋值给node.value,具体可以参照源码.
        swich(tag) ...
      }
      
更新阶段.
  • updateHostComponent.更新主函数.
  • updateHostComponent = function(
      current: Fiber,
      workInProgress: Fiber,
      type: Type,
      newProps: Props,
      rootContainerInstance: Container,
    ) {
      const oldProps = current.memoizedProps;
      if (oldProps === newProps) {
        // 新旧props一致直接return
        return;
      }
    
      const instance: Instance = workInProgress.stateNode;
      const currentHostContext = getHostContext();
      // 准备更新「重点函数」
      const updatePayload = prepareUpdate(
        instance,
        type,
        oldProps,
        newProps,
        rootContainerInstance,
        currentHostContext,
      );
      // 需要更新的props
      workInProgress.updateQueue = (updatePayload: any);
      if (updatePayload) {
        // 为当前Fiber标记需要更新
        markUpdate(workInProgress);
      }
    };
    
  • prepareUpdate函数会直接进入diffProperties,对比Props的不同返回更新数组.下面我们进入主函数.
  • 我们进入关键点.
  • 在diff中我们分为
    • 1、对于可受控组件,我们初始化它们的defaultValue,defaultChecked,value,checked值
    • 2、检测props合法性.
    • 3、比较两个props的不同之处.(这里React对style的处理是,将newProps上没有,但old上有的,设置为'',newProps上diff出来的设置为key-value).

经过beginWork和completeWork的分析,笔者认为React采取深度优先遍历的目的是:“递”阶段,我们需要diffChildFiber,找到可复用的节点提升性能,而这是“归阶段做不到的”,“归”阶段,我们又需要对于建立Dom之间的成功连接,这一操作被放在归阶段又是十分高效的(新节点不断连接新子树的过程)

「commitRoot」

commit阶段主要负责props对Fiber-Dom的属性设置以及副作用事件和生命周期函数的执行.

调度概要

  • 在commit阶段我们会进行下面几个过程,我们逐渐深入
  • 1、清空进入commit之前的副作用回调
  • 2、创建在本次commit中的副作用调度函数,(在本次commit之后执行, 下一个宏任务),在该阶段中,我们会经过「标记执行上下文CommitFlag」,「深度优先“归”阶段执行所有卸载副作用函数」,「深度优先执行所有副作用函数」注意是执行完所有卸载函数后再执行主函数,并且这里采用了树的思想,会根据subTree的flags来确定在当前节点的子树下是否还有副作用,如果没有则跳到sibling或者return(这里我们打个tag,后面来仔细分析)

  • 3、commitBeforeMutationEffects, 对于上一次渲染时被标记无法复用的Fiber,判断beforeBlur,存在执行,对于classComponent,执行getSnapshotBeforeUpdate生命周期等一系列操作

  • 4、commitMutationEffects「“归”调度,从子节点向父节点的触发顺序」.
    • 1、执行标记删除的Fiber对应dom的移除,以及自它而下的Fiber的unmounted回调.
    • 2、执行Fiber上flags的对应处理
      • 对于FunctionComponent会调度useInsertionEffect(返回函数和函数体,V18bata新增), useLayoutEffect「卸载」函数
      • 对于HostComponent会首先进行props的变化更新
    • 3、对于Ref标记进行commitDetachRef,删除指向

  • 5、commitLayoutEffects「“归”过程调度,从子节点向父节点发散」
    • 1、对于FunctionComponent会调度useLayoutEffect主函数,这里记一下,在执行时属于render的同步,会阻塞浏览器,下面讲到「useEffect」我们再比对一下,
    • 2、对于ClassComonent.
      • 通过fiber.alternate(上次渲染的fiber) === null来判断是否为首次渲染,从而执行componentDidMountedcomponentDidUpdate
      • 同时会执行在component.updateQueue上的Effects链表的回调callback
    • 3、我们在本次操作中,会使用commitAttachRef,对Ref进行赋值

上述三个重要过程都采用深度优先遍历的方式,“递”“归”化执行函数,这也是Fiber架构中的一种重要算法.

  • 笔者在在翻到这里时,却发现本次更新的useEffect是没有执行到的.通过对源码的调试发现才发现了是被作为了另一个宏任务, 这也是为什么我们说useEffect是异步调度的,因此会在useLayoutEffect之后执行

源码分析

commit之前:

  • ensureRootIsScheduled(确定任务调度) ->
  • performConcurrentWorkOnRoot(开启并发调度)->
  • renderRootConcurrent(开启beginWork-completeWork,返回出口状态)
  • finishConcurrentRender(开启commitRoot, commit阶段开始)
  • 在下文中我们大致讲解了开启commitRoot的过程,「下文不针对无源码基础读者,希望您看下文时,先了解源码,希望谅解」

  • function workLoopConcurrent() {
      // 生成缓存树
      // Perform work until Scheduler asks us to yield
      // 当workInProgress === null,表示更新完成
      while (workInProgress !== null && !shouldYield()) {
        performUnitOfWork(workInProgress);
      }
    }
    
  •   // 该函数在completeUnitOfWork(Complete阶段)执行
      // 标记当前FiberTree已complete完成
      if (workInProgressRootExitStatus === RootIncomplete) {
        workInProgressRootExitStatus = RootCompleted;
      }
    
  • // 该返回为renderRootConcurrent的状态返回,用来标识Fiber树状态
    return workInProgressRootExitStatus;
    
  • //finishConcurrentRender
    // switch (exitStatus) {
        // 正式进入commitRoot,开始commit
        case RootCompleted: {
          // The work completed. Ready to commit.
          commitRoot(root);
          break;
        }
    }
    

commit开始

下文代码片段取自笔者认为阶段重要代码片段,并不完整.

  • commit -> commitRootImpl(commit在该阶段分配了以离散事件优先级为参数的更新优先级,并执行Impl调度)
  • 清空上一次中断未完成的副作用回调
    • do {
        // 在本次更新之前,必须清空副作用数组,目的是解决副作用调度晚于本次更新,引起的执行异常.
        flushPassiveEffects();
      } while (rootWithPendingPassiveEffects !== null);
      // 清空上一次存在的副作用回调
      
  • 创建下一个宏任务以「普通事件优先级」调度执行副作用函数
    •   if (
        // 判断副作用,存在则调度
        (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
        (finishedWork.flags & PassiveMask) !== NoFlags
      ) {
        if (!rootDoesHavePassiveEffects) {
          rootDoesHavePassiveEffects = true;
          pendingPassiveEffectsRemainingLanes = remainingLanes;
          // 创建调度任务
          scheduleCallback(NormalSchedulerPriority, () => {
            flushPassiveEffects();
            return null;
          });
        }
      }
      
    • 在flushPassiveEffectsImpl函数中调度销毁副作用函数和副作用主函数,都是通过“递”“归”的方式执行
    • // flushPassiveEffectsImpl
      //   // 执行销毁函数
      commitPassiveUnmountEffects(root.current);
      // 执行挂载函数
      commitPassiveMountEffects(root, root.current);
      
    • “递”阶段,我们通过subtreeFlags(子数是否存在副作用)来确定是否有必要继续深度优先.(后面阶段也都都是这种算法)
    • // commitPassiveUnmountEffects_begin 
      // 如果不存在那么,直接进入归阶段,优化性能
      if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && child !== null) {
         ensureCorrectReturnPointer(child, fiber);
         nextEffect = child;
      } else {
         commitPassiveUnmountEffects_complete();
      }
      
    • // commitPassiveUnmountOnFiber (在“归”阶段执行,由子到父) mount同理
      function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
          switch(finishedWork.tag) {
              case FunctionComponent:
              ... {
                  if(...) // 任务中断
                  {} else {
                       commitHookEffectListUnmount(
                         HookPassive | HookHasEffect, 
                         // 这个flags监控updateQueue.effects.
                         // 如果effects为副作用函数则执行它的destory
                         finishedWork,
                         finishedWork.return,
                       );
                  }
              }
          }
      }
      
    • 该阶段我们不过多研究Hook相关内容,在后面我们会再开一篇
  • commitBeforeMutationEffects
    • 本阶段我们并没有进行关键性操作,笔者只做部分总结
    • 1、对于deletionFiber触发beforeblur事件(“递”阶段)
    • 2、对于classComponent,触发getSnapshotBeforeUpdate生命周期(“归”阶段)
  • commitMutationEffects
    • 该阶段作为commit核心部分,我们重点讲解.
    • 1、还是与上文一致,我们依然采用深度优先遍历+subTree状态标记来处理副作用.
    • 2、改阶段我们会对于diff过程中无法复用的FiberDom(deletion)进行节点移除.
      • commitMutationEffects_begin const deletions = fiber.deletions; if (deletions !== null) { for (let i = 0; i < deletions.length; i++) { const childToDelete = deletions[i]; try { // 移除当前Fiber和对应的dom,以及自它以下的Fiber的unmounted回调 commitDeletion(root, childToDelete, fiber); } catch (error) { reportUncaughtErrorInDEV(error); captureCommitPhaseError(childToDelete, fiber, error); } } }
      • 在commitDeletion内我们移除当前Fiber并且断开当前Fiber和其return的连接
      • commitDeletion主函数unmountHostComponents(移除Fiber)
        • 该函数也是React的关键点,可能您在未读源码之前和笔者一样认为移除一个Fiber是再简单不过的事,这里希望您带着以下几个问题深入学习.
        • 1、Fiber对应dom移除是如何进行的(Fiber可能对应一个组件,那也就意味着它的父元素要删除组件内的所有元素)?
        • 2、如果是HostRootFiber那么怎么去从父元素上移除它呢,或者说如果拿到其元素呢

        • 首先,对于当前Fiber而言,我们要移除它对应的相应dom,那首先就是找到它的ParentNode.

          • 可以看到这个findParent是在循环内部的,原因是对于cratePortal而言,它的挂载点是可以不为dom结构的父元素的,所以当我们遍历该Fiber时,如果该Fiber为Fragment,那么它内部如果存在portalFiber,我们就会重定义currentParent,然后进入循环,但是当我们从portal出来后,它的currentParent依旧为protalFiber的parent,而如果此时portalFiber.sibling不为null,我们就需要去重新定义currentParent,所以将它放在循环内部.
          • while (true) {
            if (!currentParentIsValid) { // 该标记是否找到父dom
                let parent = node.return;
                findParent: while (true) {
                  const parentStateNode = parent.stateNode;
                  switch (parent.tag) {
                    case HostComponent:
                      currentParent = parentStateNode;
                      currentParentIsContainer = false;
                      break findParent;
                    case HostRoot:
                      // root根实例
                      currentParent = parentStateNode.containerInfo;
                      currentParentIsContainer = true;
                      break findParent;
                    case HostPortal:
                      currentParent = parentStateNode.containerInfo;
                      currentParentIsContainer = true;
                      break findParent;
                  }
                  parent = parent.return;
                }
                currentParentIsValid = true;
                }
            
      • 在这之后,我们就会去判断当前Fiber的类型,对于HostComponent或者HostText(原生节点)我们就会卸载该Fiber节点,并DFS该Fiber.child执行其相应的回调,并通过currentParent Remove该FiberDom.
      • if (node.tag === HostComponent || node.tag === HostText) {
        // 执行当前Fiber以下的所有unMounted
        commitNestedUnmounts(finishedRoot, node, nearestMountedAncestor);
        // 判断父元素是否为容器,父dom杀出元素
        if (currentParentIsContainer) {
          removeChildFromContainer(
            ((currentParent: any): Container),
            (node.stateNode: Instance | TextInstance),
          );
        } else {
          removeChild(
            ((currentParent: any): Instance),
            (node.stateNode: Instance | TextInstance),
          );
        }
        
      • 这里HostPortal又一次被设置了,笔者认为这也是该函数的难点处
      • if (node.tag === HostPortal) {
        if (node.child !== null) {
          currentParent = node.stateNode.containerInfo;
          currentParentIsContainer = true;
          node.child.return = node;
          node = node.child;
          continue;
        }
        }
        
      • 笔者认为: PortalFiber是DFS中一个特殊分支,在findParent中我们是针对该Fiber“树”下的节点的(一般被确定),而在此处,当遇到HostPortal时,它像是被作为一颗新的树跳出当前结构执行(需要重定义).
      • 在此处,我们发现当对于ReactComponent时(这里以此为例),我们并不把它当回事😁, 因为它与dom没有关系,我们只执行它的副作用,然后查找该Fiber的对应dom结构进行remove.
      • else {
            // ReactComponent情况下
            // 执行自当前Fiber以下的unmounted
            commitUnmount(finishedRoot, node, nearestMountedAncestor);
            // 此时,当前继续循环当前Fiber.child.
            // 直到找到tag === dom的,从而在循环内删除element
            if (node.child !== null) {
              node.child.return = node;
              node = node.child;
              continue;
            }
          }
        
      •    // 单节点直接结束.
           if (node === current) {
             return;
           }
           // 当前层的Fiber已删除,返回上一层(不用考虑被删除的子层,会随父Fiber一起remove)
           while (node.sibling === null) {
           // “归”过程
             if (node.return === null || node.return === current) {
               return;
             }
             node = node.return;
             if (node.tag === HostPortal) {
              // 我们所说的,parent需要被重定义,也只有这里需要被重定义
               currentParentIsValid = false;
             }
           }
           // 这里结合findParent看,我们需要知道的是,我们只会进入parent下的第一层子节点
           // 因为在前面判断一旦遇到hostComponent,就直接删除它
           node.sibling.return = node.return;
           node = node.sibling;
        

    • 笔者花了大量的篇幅去解释该函数,原因是该函数松耦合,并且React源码大量采用这种DFS,而该函数也很好的体现了Fiber和Dom之间的关系,希望对您有帮助.

    • commitMutationEffectsOnFiber(commitMutationEffects_complete“归”阶段主函数)
      • 在该阶段React处理了Fiber的flags(节点变化)
      • 1、在该阶段对于标记了Ref的会卸载Ref绑定.
      • 2、对于PlaceMent(插入节点)会执行commitPlaceMent.
        • 1、获取HostComponentFiber(Portal,HostRoot),最近的祖先节点
        • 2、获取要被插入位置的下一个节点,如果存在调用insertBefore,否则调用appendCHild.getHostSibling(finishedWork)「666」
          • 该函数可不简单,首先自该节点开始向上查找(包括该节点)祖先Fiber的sibling,如果祖先Fiber为“原生节点”,但是sibling依然为null,那么说明没有兄弟节点
          • 如果sibling存在,我们需要判断它是否代表“原生节点”,并且还要判断是否被标记PlaceMent,如果被标记了说明该节点为插入节点,也就意味着它还没用被插入dom中,我们需要继续向后查找,除此之外还要判断HostPortal,它不存在父Fiber下,需要继续向后查找
          • “循环标准: 自底向上,遇sibling,进入sibling,父层遇“原生节点”退出,sibling为null .... ”
          • 希望读者可以阅读该函数源码,也是非常有递归思想,并且细节很多.
        • 3、insertOrAppendPlacementNode
          • 接下来我们需要插入该Fiber,该过程中我们判断当前PlaceMentFiber的tag,如果为“原生节点”,结合siblingFiber,直接插入, 否则递归当前Fiber.child,将该Fiber下的所有子dom全部插入, 仔细体会! ! !

    • commitWork(处理更新副作用)
      • 该阶段主要是处理了Fiber更新引起的副作用回调触发,以及props更新变化.
      • 我们基于FunctionComponent和HostComponent展开分析.
        • 对于FunctionComponent等,我们可以看到执行了commitHookEffectListUnmountcommitHookEffectListMount两个函数.
        • 可以看到首先执行了第一个参数均为HookInsertion | HookHasEffect的UnMount和mount(注意执行顺序).然后顺序执行了LayeOut的Ummount.
        • 那么顺序就是InsertionUnmount -> InsertionMount -> layOutUnmount
        • switch (finishedWork.tag) {
           case FunctionComponent:
           case ForwardRef:
           case MemoComponent:
           case SimpleMemoComponent: {
              commitHookEffectListUnmount(
                HookInsertion | HookHasEffect,
                finishedWork,
                finishedWork.return,
              );
           commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
           commitHookEffectListUnmount(
             HookLayout | HookHasEffect,
             finishedWork,
             finishedWork.return,
           );
          
        • 下面我们进入Unmount函数去看一下
        •   // 执行副作用函数的return函数(destory),umMount
            function commitHookEffectListUnmount(
              flags: HookFlags,
              finishedWork: Fiber,
              nearestMountedAncestor: Fiber | null,
            ) {
              // 所有副作用更新队列
              const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
              const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
              if (lastEffect !== null) {
                const firstEffect = lastEffect.next;
                let effect = firstEffect;
                do {
                  if ((effect.tag & flags) === flags) {
                    // Unmount
                    const destroy = effect.destroy;
                    effect.destroy = undefined;
                    if (destroy !== undefined) {
                      // 执行销毁函数
                      safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
                    }
                  effect = effect.next;
                } while (effect !== firstEffect);
              }
            }
          
      • 这里我们还没有对Hook展开分析,但是如果您坚持到了这里,想必对Hook一定有所了解.
      • 首先对每个Fiber而言, 它的updateQueue是环形链表,优势是一个变量就可以实现“获取firstEffect” , “获取LastEffect”, “O(1)实现add Effect”.
      • 对于FunctionComponent,它的updateQueue就是存储每个副作用函数(effect),具体而言就是useEffect(主函数和return 函数「卸载函数」),useLayOutEffect(...),以及Reactv18新增的useInsertionEffect(...).
      • 因此每个effect上都tag,标记该effect类型,到这里你应该想明白了上面执行的commitHookEffectListUnmount为什么第一个参数不一样,其实就是遍历updateQueue并执行第一个参数类型的effect,执行它的destory(主函数内的return函数),那么同理commitHookEffectListMount就是执行effect的主函数
    • 对于HostComponent.
      • 会调度commitUpdate,将complete阶段拿到的newProps更新到原生dom上(style,children,dangerouslySetInnerHTML),其余的属性会通过(for in)查找属性,如果在原生节点中存在的属性就允许直接赋值.其中还有对自定义组件的处理,这里不多赘述
      • 这里笔者着重提一个重点,就是value和defaultValue, 想必受控组件和非受控组件应该是您所熟知的,但是如果追溯原理,那就直接体现了您的思考深度,(以input为例) defaultValue并没有什么特殊,不过是原生节点的一个属性,当为一个组件设置value时,组件受控,不随“默认原生”输入事件变化,实际上input并不是没有变化,只是被重置了而已,在scheduleEvent阶段,当case ‘input’时,会调用ReactDOMInputUpdateWrapper,在该函数中,会执行value和defaultValue的设置,并且会比较node.value(输入完成后的值)和props.value一致性,如果不一致,props.value会覆盖node.value,所以导致输入失效.而该组件也就是著名的受控组件(checked同理)
      • 上面内容本应该在事件机制中阐述,但是笔者发现,在该阶段的updateProperties也已经存在该内容,所以就在本篇阐述了,希望您能结合下文的事件机制深入体会.
  • commitLayOutEffects
    • 此时FinishedWork已经全部挂载完成,开始执行mount(update)回调
    • commitLayoutEffectOnFiber(“归”阶段,由子到父)
      •    // 该LayoutMask === Update | Callback | Ref | Visibility(标记是否有更新);
           if ((finishedWork.flags & LayoutMask) !== NoFlags) {...}
        
      • 在该函数的结尾处(这里埋个伏笔),我们会判断Ref,并设置ref引用,顺便说一句ref为对象或者函数的目的就是实现数据的正确引用.
      • if (finishedWork.flags & Ref) {
          commitAttachRef(finishedWork);
        }
        
      • 这里我又用到了commitHookEffectListMount,它的flags = HookLayout | HookHasEffect.
      • 现在我们知道了我们的执行顺序为 InsertionUnmount -> InsertionMount -> layOutUnmount -> layOutUnmount.
      • 结合上文我们知道该函数末尾会对标记Ref的对象设置该Ref的引用.并且我们Ref都会用来标记子Dom,并且函数和其他过程一样为子到父的执行顺序,这也就以为这当我们还在执行到父Fiber的useLayOutEffect时,它所设置的ref已经被设置了指针指向.
      • 综上,我们可以得出,在layOutEffect执行时,我们已经设置了Ref指向这也是它区别于insertionEffect的地方
      • switch (finishedWork.tag) {
         case FunctionComponent:
         case ForwardRef:
         case SimpleMemoComponent: {
              commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
        }
        
      • 对于classComponent
        • 会判断current(上一次更新的Fiber)是否存在来决定调用componentDidMount或componentDidUpdate.
        • 然后会调用commitUpdateQueue对所有setState的回调函数进行遍历执行

    • 到此为止,当前“宏任务”下的commit函数系列已经完成,是的,还记得我们之前的说的flushPassiveEffects函数吗,该函数会执行副作用调度(useEffect),同样的,它也是首先执行完所有effect.destory,然后执行create.因此我们说的useEffect实际上是独立于当前宏任务执行的,这也是为什么我们有时在useEfect中设置样式时,会出现闪屏,如果要解决该问题,应该使用useLayOutEffect,在LayOutEffect中,如果setState,会生成一个新的syncCallback,在flushSyncCallbacks中添加到执行队列,同步进行再一次更新(后面会深入分析)

好了,我们的commit阶段解读结束了,这一部分是笔者撕的较为细的,因为重要操作较多,并且实际开发中也常经过这些过程.开冲hook,😁


ReactHook篇

en~~,笔者在写到这里时,有点乏了,打工人真的很辛苦,好吧,我们回到正题,首先在看到这一篇的时候希望您最起码对整个渲染过程有有大体的认识了,hook作为现阶段react的主要开发模式,我想了解它底层的原理是很重要的(主要为了吹牛),笔者在撕源码之前,曾看过卡颂大佬的文章,笔者认为卡颂大佬的文章可以作为启蒙篇,对于部分内容如果不深入还是有疑惑的.我们开冲了!

Hook基础篇

  • 首先: 您知道Hook执行的阶段吗? 您知道hook为什么不允许卸载嵌套语句中吗?

  • 回答第一个问题: 在beginWork阶段,如果您认真阅读了React,那么您一定知道该阶段是在生成FiberTree的,那么这都是依赖于拿到renderFiber的.而对于FunctionComponent,就必须执行component().

  • 第二个问题我们后面解释.

  • renderWithHook(FuncionCompnent主函数)

  •  nextChildren = renderWithHooks( // 执行当前Function
           current,
           workInProgress,
           Component,
           nextProps,
           context,
           renderLanes,
         );
    
  • 可以看到对于FunctionComponent而言,执行Function阶段是从父到子的,但是如果您有仔细看commit阶段的内容,就能知道执行Function内部的effect却是在commit的,所以现在我们知道对于Hook而言,执行该函数值只是在为commit做准备

  • React对于mount和update有两套hook

  •   // 全局调度器列表
      ReactCurrentDispatcher.current =
        current === null || current.memoizedState === null
          ? HooksDispatcherOnMount
          : HooksDispatcherOnUpdate;
      // 首次挂载时hook
      const HooksDispatcherOnMount: Dispatcher = {
          readContext,
          useCallback: mountCallback,
          useContext: readContext,
          useMemo: mountMemo,
          useReducer: mountReducer,
          useRef: mountRef,
          useState: mountState,
          ....
      }
      // 更新时hook
      const HooksDispatcherOnUpdate: Dispatcher = {
          readContext,
          useCallback: updateCallback,
          useContext: readContext,
          useEffect: updateEffect,
          useImperativeHandle: updateImperativeHandle,
          useInsertionEffect: updateInsertionEffect,
          useLayoutEffect: updateLayoutEffect,
          useMemo: updateMemo,
          ...
      }
    
  • 下面我们来讲一下Hook在Function中的存在方式,Hook会以对象节点的形式存在Fiber.memoizedState中.

    •    const hook: Hook = {
           // 存储的值
           memoizedState: null,
           baseState: null,
           // 上次更新的队列
           baseQueue: null,
           // hook更新队列(循环链表 便于插入和找到头部)
           queue: null,
           // 与下一个hook形成单链表
           next: null,
         };
         if (workInProgressHook === null) {
           currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
         } else {
           workInProgressHook = workInProgressHook.next = hook;
         }
         return workInProgressHook;
         }
      
  • 这里不可以将hook.memoizedState和fiber.memoizedState弄混,前者表示当前hook的相关信息(输出结果,依赖等),后者表示当前Fiber下存在的hook单链表,它们是毫不相干的.

  • 这里笔者顺带提一下updateWorkInProgressHook,该函数主要是复用currentFiber的hook,因为我们在cloneFiber之后就丢失了hook链表,而在updateHook时,我们的currentFiber上是存在相应的Hook的所以我们就可以通过alternate拿到current.memoizedState(上次更新的Hook链表),之后直接clone oldHook,这时我们上面的第二个答案就出来了,之所以不允许hook写在嵌套,是因为每次执行component时,拿到clone的oldHook是通过hook.next获取的,如果由于嵌套的原因,使本次hook链表长度和mount时不一致,就会使clone出错

  • 我们再来看一下hook中的属性.

    • memoizedState(存储当前hook的重要信息,依赖等)
    • baseState 表示基于该数据进行更新
    • baseQueue 上次被中断的更新队列
    • queue 存储更新队列pending,dispatch,lane等信息.
  • Hook全面解析

useCallback

  • mountCallback
    •   const nextDeps = deps === undefined ? null : deps;
        // 保存hook状态
        hook.memoizedState = [callback, nextDeps];
        return callback;
      
  • updateCallback
    •   const hook = updateWorkInProgressHook(); 
        const nextDeps = deps === undefined ? null : deps;
        // 参照上面mount的数据
        const prevState = hook.memoizedState;
        if (prevState !== null) {
          if (nextDeps !== null) {
            // 上一次的依赖
            const prevDeps: Array<mixed> | null = prevState[1];
            // 比较依赖变化,如果为未变化,直接复用,(Object.is)
            if (areHookInputsEqual(nextDeps, prevDeps)) {
              return prevState[0];
            }
          }
        }
        // 创建
        hook.memoizedState = [callback, nextDeps];
        return callback;
      
    • 注: 对于useCallback判断依赖的原因是每一次的函数执行的作用域是不同的,这意味着如果依赖变化,但是函数未重定义,就会使数据指向不符合预期,所以React在这里既保证了效率有保证了函数的正确性.

useEffect

  • mountEffect
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
    return mountEffectImpl(
      PassiveEffect | PassiveStaticEffect,
      HookPassive,
      // 主函数
      create,
      deps,
    );
}

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 表示当前Fiber存在副作用回调,为commit阶段判断是否需要深入做准备
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}
  • 这里关注一下上面的pushEffect,这步操作不仅更新了hook,而且更新了updateQueue(副作用循环链表).
function pushEffect(tag, create, destroy, deps) {
  // 生成effect节点
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };
let componentUpdateQueue =(currentlyRenderingFiber.updateQueue);
  if (componentUpdateQueue === null) {
  // 创建副作用循环链表, 对象只存在lastEffect用于指向最后一个副作用
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    // 循环链表
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
  // 更新循环链表
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}
  • updateEffect
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    // 这时候destoy必然已经在上一次commit阶段执行
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 缺少HookHasEffect字段,表示当前hook依赖不变,commit时不必执行
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }
  // 表示当前fiber需要执行当前副作用函数.
  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}
  • 其实updateEffect和uodateCallback并无大的差异,但还是有几点巧妙的地方,我们来看一下.
    • 1、destroy = prevEffect.destroy;这里可以看到我们对销毁函数进行了赋值,这是因为如果currentFiber存在,那么在mount时必然已经执行过该副作用函数,在执行结束我们会将返回值存在该hook.destory中,在这里我们就可以直接复用它,在commit阶段的unmountEffect函数中执行.
    • 2、HookHasEffect,可以看到的是该flags对于updateQueue而言,是mount和update的区别,如果您仔细看过笔者commit篇的文章,就可以发现React在执行副作用函数时是用flags来遍历updateQueue,只有flags全等才会得到执行,而该标记就是决定当前Fiber是否因依赖变化而需要执行回调的flags.
      • 那么这时您可能会有疑问了,那为什么在依赖不变时,依然需要pushEffect进入updateQueue,这么做是因为在组件销毁时,需要调用所有effect的destory函数,所以即使依赖不变,也需要pushEffect

useLayoutEffect和useInsertionEffect和useEffect基本一致,只有flags和Fiberflags有区别(标记函数执行时机),本文不多做赘述.

useImperativeHandle

  • 简析: 子组件使用useImperativeHandle可以实现子向父传参,该函数往往与forwardRef共用
  • mountImperativeHandle
function mountImperativeHandle<T>(
  ref,
  create: () => T,
  deps: Array<mixed> | void | null,
): void {
  const effectDeps =
    deps !== null && deps !== undefined ? deps.concat([ref]) : null;
    
  let fiberFlags: Flags = UpdateEffect;
  // 与layout一致的执行优先级
  return mountEffectImpl(
    fiberFlags,
    HookLayout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps,
  );
}

function imperativeHandleEffect<T>(
  create: () => T,
  ref,
) {
    // 设置ref指向
  if (typeof ref === 'function') {
    const refCallback = ref;
    const inst = create();
    refCallback(inst);
    // 返回销毁函数
    return () => {
      refCallback(null);
    };
  } else if (ref !== null && ref !== undefined) {
    const refObject = ref;
    const inst = create();
    refObject.current = inst;
    return () => {
      refObject.current = null;
    };
  }
}
  • 其实这里可看出它的执行时机和方式和useLayOut基本一致,这里笔者顺带提一嘴,我们看到它的更新标记是updateEffect,再看useEffect是Passive,如果您仔细阅读了commit阶段,就可以知道,update标记是在当前宏任务下判断执行的(commitWork),而Passive会被创建新的任务调度在下一次执行.那么这里使用layOut的优先级也是可以理解的,因为我们认为在useEffect中是已经标记完ref指向的.
  • 那么其实该函数和在useLayOut中设置ref.current一致,但这不是为了React16后从命令式编程到声明式编程的一小步嘛

updateImperativeHandle依然是比对依赖决定ref指向是否变化,希望您自行翻译.

useMemo

基本与useCallback一致,只有hook.memoizedState保存的值又变量变为了方法.

useReducer(重点,代表性hook)

useReducer是hook中极具代表性(这个真的可以吹牛😁)

  • useReducer实际是redux的纯函数思想的复用,都是Dan大神的杰作,对于判断处理多个子值的复杂入参极其友好.这里笔者并不多做介绍,我们直接从源码角度来看React的实现.
  • mountReducer
    •   function mountReducer<S, I, A>(
          reducer: (S, A) => S, // reducer, 纯函数
          initialArg: I, // 默认值
          init?: I => S, // 以初始值为参数,将返回作为初始值
        ): [S, Dispatch<A>] {
          const hook = mountWorkInProgressHook();
          // 默认值
          let initialState;
          if (init !== undefined) {
            initialState = init(initialArg);
          } else {
            initialState = ((initialArg: any): S);
          }
          hook.memoizedState = hook.baseState = initialState;
          // 更新队列
          const queue: UpdateQueue<S, A> = {
            pending: null,
            interleaved: null,
            lanes: NoLanes,
            dispatch: null,
            lastRenderedReducer: reducer,
            lastRenderedState: (initialState: any),
          };
          hook.queue = queue;
          const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(
            null,
            currentlyRenderingFiber,
            queue,
          ): any));
          return [hook.memoizedState, dispatch];
        }
        // useReducer的第二个出参
        function dispatchReducerAction<S, A>(
          fiber: Fiber,
          queue: UpdateQueue<S, A>,
          action: A,
        ) {
      
          const lane = requestUpdateLane(fiber);
          // 本次更新,
          const update: Update<S, A> = {
            lane,
            action,
            hasEagerState: false,
            eagerState: null,
            next: (null: any),
          };
          // 判断render是触发Reducer
          if (isRenderPhaseUpdate(fiber)) {
            enqueueRenderPhaseUpdate(queue, update);
          } else {
            // 在事件或者其他宏任务中触发. 执行任务调度
            enqueueUpdate(fiber, queue, update, lane);
            const eventTime = requestEventTime();
            const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
            if (root !== null) {
              entangleTransitionUpdate(root, queue, lane);
            }
          }
          markUpdateInDevTools(fiber, lane, action);
        }
      
    • 看了代码也许您有点晕,甚至无法理解,下面笔者来解析一下上面的操作.
      • 1、第三个参数init会作为默认rducer在初始化时将initialArg传入该函数,再将返回值作为默认值
      • 2、对于依据入参获取出参的纯函数hook(useState类似「后面讲」),React维护了hook.queue,以提升性能.
        • 可以看到在dispatch中我们执行了enqueueUpdate,这其实就是在将本次更新作为节点放入queue.pending中(循环链表形式存在),(具体分析我们会在update时介绍).

        • 也许你注意到了isRenderPhaseUpdate函数,这其实是在判断当前dispatch是否是在当前render阶段,试想如果当前dispatch是在当前render阶段,那么我们不断的scaduleUpdate,就会进入死循环.React可不笨,我们在enqueueRenderPhaseUpdate()时会设置didScheduleRenderPhaseUpdateDuringThisPass = true.

          • 看下面的代码就能知道,React会继续循环执行Component,如果每次执行都有setReduce,那么就直接抛出错误.
          • let children = Component(props, secondArg);
            // 判断render阶段的update(dispatch操作等) 🌹
            // 对于在render阶段进行的dispatch会循环执行
             // Check if there was a render phase update
             // 
            if (didScheduleRenderPhaseUpdateDuringThisPass) {
              // 记录重绘次数
            let numberOfReRenders: number = 0;
            do {
              didScheduleRenderPhaseUpdateDuringThisPass = false;
             localIdCounter = 0;
            
             if (numberOfReRenders >= RE_RENDER_LIMIT)
                    throw Error(...)
             }
             ....
             children = Component(props, secondArg);
            } while (didScheduleRenderPhaseUpdateDuringThisPass);
            
      • 好了,现在我们经过了mount阶段,关键点:updateDispatch一直在pending中
  • rerenderReducer
    • 该函数会在render时执行dispatch而发生的重新执行component(),作为useReducer执行.
    • 该函数会遍历由dispatch引起的update,直接循环执行,并重新赋值newState.
    • 具体源码,希望读者自己分析
  • updateReducer
    • 该函数会在更新Fiber时执行,下面让笔者带你进入代表性hook的世界.
    • 1、首先和其他hook一样,都是拿到当前Fiber上的下一个hook.
      const hook = updateWorkInProgressHook();
      const queue = hook.queue;
    
    • 2、还记得我们之前的lastRenderedReducer吗,它之所以叫这个名字就是表示,它的reducer是依赖与最后一个reducer函数的.
      queue.lastRenderedReducer = reducer;
    
    • 那么baseQueue还记得吗?这里笔者油爆枇杷一下,可以看到下面代码,我们讲上一次更新的baseQueue和pending解开链表,装到了baseQueue上,这就意味着此时,baseQueue上已经承载了所有update.
    const current: Hook = (currentHook: any);
    // 上次未更新完的queue
    let baseQueue = current.baseQueue;
    // 本次更新的hook
    const pendingQueue = queue.pending;
    if (pendingQueue !== null) {
    if (baseQueue !== null) {
      // 合并本次更新和上次更新
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    // 更新baseQueue
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
    }
    
    • 此时我们已经拿到了updateQueue,接下来就是遍历,但是如果您对React有一定的了解,那么就一定听说过React优先级调度问题
      if (baseQueue !== null) {
     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;
       // 判断渲染优先级和更新优先级,如果该更新不在本次更新车道内
       // 则本次update放入baseQueue中.并跳过本次更新
       if (!isSubsetOfLanes(renderLanes, updateLane)) {
         const clone: Update<S, A> = {
           lane: updateLane,
           action: update.action,
           hasEagerState: update.hasEagerState,
           eagerState: update.eagerState,
           next: (null: any),
         };
         if (newBaseQueueLast === null) {
           newBaseQueueFirst = newBaseQueueLast = clone;
           newBaseState = newState;
         } else {
           newBaseQueueLast = newBaseQueueLast.next = clone;
         }
         // 将本次更新优先级合并
         currentlyRenderingFiber.lanes = mergeLanes(
           currentlyRenderingFiber.lanes,
           updateLane,
         );
         markSkippedUpdateLanes(updateLane);
       } else {
       // 再一次clone
         if (newBaseQueueLast !== null) {
           const clone: Update<S, A> = {
             lane: NoLane,
             action: update.action,
             hasEagerState: update.hasEagerState,
             eagerState: update.eagerState,
             next: (null: any),
           };
           newBaseQueueLast = newBaseQueueLast.next = clone;
         }
         // 没啥用, 兼容setState,下面讲
         if (update.hasEagerState) {
           newState = ((update.eagerState: any): S);
         } else {
         // 进入lastRenderedReducer,循环执行
           const action = update.action;
           newState = reducer(newState, action);
         }
       }
       update = update.next;
     } while (update !== null && update !== first);
    
     if (newBaseQueueLast === null) {
       newBaseState = newState;
     } else {
       newBaseQueueLast.next = (newBaseQueueFirst: any);
     }
    
     // 判断是否需要更新
     if (!is(newState, hook.memoizedState)) {
       markWorkInProgressReceivedUpdate();
     }
    
     hook.memoizedState = newState;
     hook.baseState = newBaseState;
     hook.baseQueue = newBaseQueueLast;
     queue.lastRenderedState = newState;
    }
    
    • 笔者在上文中已经做了注释,有个重要的点笔者讲一下,可以看到在源码中,对于优先级较低的以及baseQueue不为空的情况,会将本次更新放入baseQueue,那么对于baseQueue不为空的情况都要将update放入队列,可能有点疑惑,笔者想说的是「React的每次更新都是依赖当前环境做出的变化」,而如果当在某次更新中你跳过了优先级低的updateA,而执行了优先级高的updateB,然后又跳过了优先级低的updateC,那么在下次执行时就不是理想的「A->B->C」,而是「A->C」,这也是React为什么baseQueue不为空就需要将当前update放入
    • 那这不是多此一举吗,下次还是要执行全部,是的,但是如果你从用户角度思考,你是希望快速看到响应的,React在保证快速响应的同时又保证了数据的正确性,者就是著名的心智模式

笔者在解读该篇时,有跳过优先级处理和cocurrent模式下的交叉渲染,我们后(现)面(在)复(不)盘(会).


useRef

  • useRef是一种近乎原生hook,就是将hook的状态指向了某一个值,它用对象的方式,将它放存在堆内存中,保证指向和修改正确
  • MountRef
function mountRef<T>(initialValue: T): {|current: T|} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}
  • updateRef
function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

useState

  • 这可能是用的最多的hook了,ReactHook以它为状态代替了类组件的state.在源码上它其实借鉴了useReducer的思路,笔者这里只写一下细微的差别吧,不想水代码了.
  • mountState
    • mountState和mountReducer相比,无非是允许useState参数为函数,可以判断初始化,而对于useReducer而言,则将函数的情况放了最后一个参数(init),通过判断最后一个参数,将初始值作为init的参数放入后,将出参作为参数返回.
if (typeof initialState === 'function') {
    initialState = initialState();
}
  • upDateState
    • 看到这里想必你已经懵了,它居然一丝不挂的抄了useReducer.
    • 作为最为熟知的Hook,我们还是稍微体面的说一下它的原理的.
    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;
    }
    
    • 如果你对useReducer比较熟知,那么你应该知道在这里更新状态时的useReducer的第一个参数是没有意义的,真正有用的都放在了hook队列中
    • 不知道你在使用setState时,有没有这种场景,你想bactchUpdate某一个state,但是当前设置的state的值是依赖上一次更新的,但是结果却和预计有差别
      • 可以看下面代码, 当我们以值的形式更新时,它是不满足我们的预期的, 但是当我们使用函数式的时候, 则是满足预期的
        const [value, setValue] = useState(1);
        <button onClick={() => {
            setValue(value+1); // 2
            setValue(value+1); // 2
            // setValue((value) => value+1); // 3
        }}></button>
      
        useEfect(() => {
            console.log(value); 
        }, [value])
      
      • 原因就在updateReducer的下面这句话,可以看到我们的basicStateReducer中会判断传入action类型,如果是值则直接赋值,如果是函数则将本次循环之前设置的值(「newState」)作为参数返回.
        newState = reducer(newState, action);
        

&&useTransition&&

  • 该hook花了笔者大量的时间,由于其相关到了优先级篇,难度较高,如果您并没有那么希望深耕源码,笔者甚至并不建议您读下去,结合网络的资料,笔者并不认为有大部分的面试官能够了解到这个阶段,如果您有更高的自我追求,那么我们继续.

  • 该hook的出现一定程度上是为了解决用户感受问题,我们知道任务队列中存在render和执行脚本,如果脚本执行过长,就会导致在当前帧下render被覆盖,从而产生卡顿.

  • 试想下面场景: 我们的「输入」「由输入产生的页面更新」,我们其实更在意的是输入实时,而数据更新延时是可以接受的,但正常逻辑下这两次执行是同时的,也就是都会进入SyncQueue.那么该hook的出现就是为了解决该问题.(仔细思考,其实这已经涉及到了优先级的问题了.),

  • mountTransition:

function mountTransition(): [boolean, (() => void) => void] {
  const [isPending, setPending] = mountState(false);
  // The `start` method never changes.
  // startTransition见下文
  const start = startTransition.bind(null, setPending);
  const hook = mountWorkInProgressHook();
  hook.memoizedState = start;
  return [isPending, start];
}

// 更新时并没有多余的操作
function updateTransition(): [boolean, (() => void) => void] {
  const [isPending] = updateState(false);
  const hook = updateWorkInProgressHook();
  const start = hook.memoizedState;
  return [isPending, start];
}
  • 那么设置transition = 1的作用是什么,又是如何做到延迟执行的呢.
const App = function() {
  const [value, startTransition] = useTransition();
  const [v,setV] = useState(0);
  const [m,setM] = useState(0);

  return <div>
    <button onClick={() => {
      debugger
      startTransition(() => {
        setV(1);
      })
      setM(123);
    }}>{v}</button>
    {m}
  </div>
}
  • 以此为例: 点击button,界面的渲染过程是(0 0) -> (0 123) -> (1 123);『如果不是这样的话,我们就在讲废话了.』

  • 1、首先当前点击会作为离散事件优先级执行调度.
function dispatchDiscreteEvent(
    domEventName, 
    eventSystemFlags,
    container, 
    nativeEvent
) {
    // 通过事件优先级 映射 更新优先级.
    setCurrentUpdatePriority(DiscreteEventPriority); // 1
    // 执行事件
    dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
}
  • 2、我们跳过batchUpdate等内容,分析startTransition
 function startTransition(setPending, callback) {
 // 获取当前优先级
    const previousPriority = getCurrentUpdatePriority(); // 1
// 我们给当前setPending至少持续事件优先级(对应mousemove等持续事件,小于点击等离散事件优先级)
    setCurrentUpdatePriority(
      higherEventPriority(previousPriority, ContinuousEventPriority),
      );
    // 设置pending的优先级是较高的.
    setPending(true);
    // 这里看到我们设置了一个全局过渡标记位
    const prevTransition = ReactCurrentBatchConfig.transition;
    ReactCurrentBatchConfig.transition = 1;
    try {
    // 这里执行的时候我们的标记量为trnasition
      setPending(false);
      callback();
    } finally {
      setCurrentUpdatePriority(previousPriority);
      ReactCurrentBatchConfig.transition = prevTransition;
    }
}

笔者在解析useTransition时发现,其实相关奥秘藏在了dispatchSetState中,所以笔者特地抽离了这块内容

function dispatchSetState(fiber, queue, action) {
    requestUpdateLane(fiber) // 获取本次update优先级
    var update = {
        lane: lane,
        action: action,
        hasEagerState: false,
        eagerState: null,
        next: null
    };
    // .... 略去不相关过程
    enqueueUpdate$1(fiber, queue, update); // 更新循环队列queue.
    
    // 调度更新了.
    var root = scheduleUpdateOnFiber(fiber, lane, eventTime);  
    if (root !== null) {
      // 对于transitionLane进行的操作.(startTrantion「1」)
      entangleTransitionUpdate(root, queue, lane); // 🌹
    }
}

function scheduleUpdateOnFiber(fiber, lane, eventTime) {
  checkForNestedUpdates(); // 判断更新次数
  // 标记当前节点的lanes和自其以上的所有节点的childLanes,
  // 优化查询在更新时从rootFiber以下的待更新节点
  var root = markUpdateLaneFromFiberToRoot(fiber, lane);
  
 // 增加根节点的pendingLanes.
 // 并且在这里我们会将本次优先级(根据优先级的高低)通过巧妙的位运算
 // 作为索引设置root.eventTimes(车道过期事件列表)
  markRootUpdated(root, lane, eventTime);
  
  // 见React优先级篇🌹***
  ensureRootIsScheduled(root, eventTime);
  
      //  对于当前环境为特殊宏任务中,settimeOut()类似,直接调度任务列表,同步执行.
    if (lane === SyncLane && executionContext === NoContext && (fiber.mode & ConcurrentMode) === NoMode && // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
    !( ReactCurrentActQueue$1.isBatchingLegacy)) {
      resetRenderTimer();
      flushSyncCallbacksOnlyInLegacyMode();
    }
}

function entangleTransitionUpdate(root, queue, lane) {
// 缠绕当前queue.lane
  if (isTransitionLane(lane)) {
    var queueLanes = queue.lanes;
    // 当前车道和该stateQueue的车道求(&),获取到queue上存在的本次车道的更新
    queueLanes = intersectLanes(queueLanes, root.pendingLanes);
    // 将当前过渡车道合并到新的队列车道
    var newQueueLanes = mergeLanes(queueLanes, lane);
    queue.lanes = newQueueLanes;
    // 车道缠绕,将当前车道(|)到root.entangledLanes.
    // 再将entangledLanes处理后展开的所有车道缠绕上当前的lane
    markRootEntangled(root, newQueueLanes);
  }
}
  • 可以看到我们在dispatchSetState中设置了一系列优先级,并最后以ensureRootIsScheduled结尾,为了减少文章的臃肿程度,我们将其内部执行放在了下文.望您移步查看ensureRootIsScheduled.
  • &&useTransition&&(姊妹篇)
    • 在ensureRootIsScheduled中笔者详细讲解了确保任务调度的过程.那下面笔者就分析一下在useTransition中的transition标记量问题.
    • 我们以useTransition中的例子进行解析(继续看下去的前提是您已经看完了稳重useTransition相关内容).
      • 我们现在知道了transition标记量使得我们将本次更新lane缠绕到queue.lanes中.下面我们来看该缠绕的作用.
      • 并且在ensureRootIsScheduled中发起了因为“点击”形成的离散事件优先级出发的的scheduleCallback,其中会调度performSyncWorkOnRoot,执行workLoop创建新的FiberTree.
      • 那么相信您一定还记得在此执行该FunctionComponent的时机吧(beginWork阶段).
      • 在该阶段我们就会再次执行到该函数,此时我们的useTransition已经变为了updateTransition
      function updateTransition(): [boolean, (() => void) => void] {
        const [isPending] = updateState(false); // 奥秘就藏在这里.
        const hook = updateWorkInProgressHook();
        const start = hook.memoizedState;
        return [isPending, start];
      }
      
      function updateReducer<S, I, A>(
        reducer: (S, A) => S,
        initialArg: I,
        init?: I => S,
      ): [S, Dispatch<A>] {
          ...不再展开,可以在updateReducer模块查看.
          // 这里我们遍历了queue.update,最终将不再本次更新车道的update跳过.
          if (!isSubsetOfLanes(renderLanes, updateLane)) {
            const clone: Update<S, A> = {
            lane: updateLane,
            action: update.action,
            hasEagerState: update.hasEagerState,
            eagerState: update.eagerState,
            next: (null: any),
          };
          if (newBaseQueueLast === null) {
            newBaseQueueFirst = newBaseQueueLast = clone;
            newBaseState = newState;
          } else {
            newBaseQueueLast = newBaseQueueLast.next = clone;
          }
          // 🌹,新的workInProgressFiber的车道被该跳过的更新缠绕.
          currentlyRenderingFiber.lanes = mergeLanes(
            currentlyRenderingFiber.lanes,
            updateLane,
          );
          markSkippedUpdateLanes(updateLane);
        }
      
      • commit
      // 在commitRoot阶段中
      function commitRootImpl(root, renderPriorityLevel) {
          // 那么在commit阶段我们就可以获取到剩下的车道了.
          var remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
          // 我们重新设置pendingLanes = remaininglanes
          markRootFinished(root, remainingLanes);
          
          // 在commit结束之后我们确定下一个任务紧急任务.
          ensureRootIsScheduled(root, now());
      }
      function ensureRootIsScheduled(root, currentTime) {
           // 获取下一个任务.(transition模式下为0b1000000起始值)
           var newCallbackPriority = getHighestPriorityLane(nextLanes);
           // 紧急任务
           if (newCallbackPriority === SyncLane) {
             ....
           } else {
              var schedulerPriorityLevel;
              // 通过车道换取触发事件优先级, 再换取任务调度优先级
              switch (lanesToEventPriority(nextLanes)) {
                ....
                // 闲置优先级调度
                case IdleEventPriority:
                  schedulerPriorityLevel = IdlePriority;
                  break;
      
                default:
                  schedulerPriorityLevel = NormalPriority;
                  break;
              }
              // 存储任务调度
              newCallbackNode = scheduleCallback$1(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
           }
      }
      ... 剩下的内容请移步优先级与调度篇
      
    • 总结例子中的执行过程
    • 1、dispatchSetStatePending = true.
    • 2、设置transition = 1,标记后续更新为过渡更新.
      • 2.1、获取优先级为过渡优先级,同时本次update.lane为过渡优先级.
    • 3、在执行performSyncWorkOnRoot时(由lane === 1触发的更新),继续执行renderRootSync(beginWork过程),其中会执行component(),在再一次updateTransition时,拿到在该Fiber上的相应hook,这时在该hook.queue的update.lane会再一次传染给当前Fiber.lane.(去除update.lane和本次renderLane一致的lane)
    • 4、在commitRootImpl后,会再一次发起ensureRootIsScheduled确定下一个最紧急的车道lane.
    • 5、之后将该车道映射相应的任务以及执行时机,发起异步调度.这就很好的做到了延时调度和任务插队

dispatchSetState的实质是当前Fiber上hook.queue新增了一个节点,并执行拥有调度任务权限的ensureRootIsScheduled.

&&React优先级与调度&&

笔者在写这一块的文章时是最犯怵的,优先级作为React性能优化的重要手段,其实是非常值得探索的,但其实它的参考资源和涉及变量以及位运算相关是在是React源码的另一个层次,当然如果您与笔者能彻底了解这一块,那在您的简历上就会留下非常浓墨重彩的一笔.

requestUpdateLane

获取Fiber更新的车道


function requestUpdateLane(fiber) {
     var mode = fiber.mode; // 标记当前fiber的更新模式,这里我们用了renderRoot,所以是并发模式
     // 对于非并发模式下, 直接返回同步优先级,所有任务都为紧急任务.
      if ((mode & ConcurrentMode) === NoMode) {
        return SyncLane;
      } else if ( (executionContext & RenderContext) !== NoContext && workInProgressRootRenderLanes !== NoLanes) {
        // render阶段发生的更新
        // 最高优先级
        return pickArbitraryLane(workInProgressRootRenderLanes);
      }
     
     // 这里就是在拿全局的过渡状态了.
     var isTransition = requestCurrentTransition() !== NoTransition;
     if (isTransition) {
      if (currentEventTransitionLane === NoLane) {
        // 拿到过渡方式的事件车道
        // 下面的函数,我们暂时不进入了, 希望您自己去撕,这里就是在拿当前过渡优先级
        // 该值起始值为Ob0001000000(64),为了保证唯一性,会不断左移,峰值时环状赋值
        // 值得注意的是,该值大于所有事件调度lanes(优先级低于所有事件调度lanes)
        currentEventTransitionLane = claimNextTransitionLane();
      }
      return currentEventTransitionLane;
     }
     // 当前更新优先级, 我们在之前已经set,所以这里直接拿到“1”
  var updateLane = getCurrentUpdatePriority();

  if (updateLane !== NoLane) {
    return updateLane;
  }
  var eventLane = getCurrentEventPriority();
  return eventLane;
}

ensureRootIsScheduled

  • 该函数可以说对React的任务调度有承上启下的作用,它用于确定下一个最紧急的任务并发起调度.在下文中我们可以看到车道(lanes) -> 任务优先级(callbackPriority,最紧急车道) -> 事件优先级(eventPriority) -> 调度优先级(schedulerPriorityLevel) -> 形成task
function ensureRootIsScheduled(root, currentTime) {
  var existingCallbackNode = root.callbackNode; // 当前准备调度“任务“
  // 遍历pendinglanes,通过keyValue获取不同类型车道的任务过期事件.
  // 设置root.expirationTimes中
  // 对于已经过期的任务,将车道作或运算到root.expiredLanes
  markStarvedLanesAsExpired(root, currentTime);
  // 拿到下一个任务调度车道, 后面分析
  var nextLanes = getNextLanes(root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
   // 本次确认调度的任务优先级(获取最高优先级)
  var newCallbackPriority = getHighestPriorityLane(nextLanes);
  // 当下的任务优先级
  var existingCallbackPriority = root.callbackPriority;
  // 对于两次任务优先级一致的调度,后续合并调度
  if(newCallbackPriority === existingCallbackPriority) ...return;
  // 对于存在的紧急任务, 取消等待调度的concurrent任务
  if (existingCallbackNode != null) {
    cancelCallback$1(existingCallbackNode);
  }

  // 任务节点
  var newCallbackNode;
  
  // 执行到这里时,1、任务调度队列位空, 2、本次任务调度优先级较高.
  if(newCallbackPriority === SyncLane) {
    if (root.tag === LegacyRoot) { 
      // 对于非concurrent模式下的任务
      // 我们会设置includesLegacy为true,并将任务放入SyncQueue(紧急调度队列)
      // 该标记的作用是在执行本次事件batchUpdate后的finally中会判断执行
      // includesLegacy为true时,直接遍历syncQueue.
      if ( ReactCurrentActQueue$1.isBatchingLegacy !== null) {
        ReactCurrentActQueue$1.didScheduleLegacyUpdate = true;
      }
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      // concurrent模式下,includesLegacy为false, 也放入SyncQueue中
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }

    {
      // Flush the queue in a microtask.
      if ( ReactCurrentActQueue$1.current !== null) {
        ReactCurrentActQueue$1.current.push(flushSyncCallbacks);
      } else {
        // 将调度SyncQueue放入微任务队列
        scheduleMicrotask(flushSyncCallbacks);
      }
  } else{
  // 任务调度优先级
    var schedulerPriorityLevel;
    // 我们拿最紧急车道换取事件优先级 -> 映射任务调度优先级
    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediatePriority;
        break;

      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingPriority;
        break;

      case DefaultEventPriority:
        schedulerPriorityLevel = NormalPriority;
        break;

      case IdleEventPriority:
        schedulerPriorityLevel = IdlePriority;
        break;

      default:
        schedulerPriorityLevel = NormalPriority;
        break;
    }
    // 等待调度的任务节点scheduleTask.
    // 其中实际是执行了unstable_scheduleCallback.
    // 通过调度优先级映射过期时间,形成一个task.
    newCallbackNode = scheduleCallback$1(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
    root.callbackPriority = newCallbackPriority;
    root.callbackNode = newCallbackNode;
  }
}

unstable_scheduleCallback

调度中用到小顶堆的数据结构,望您有一定的基础,笔者也会兼容不懂小顶堆的读者

  • 调度任务函数,即上文scheduleCallback$1,任务调度入口.
  • 1、该函数拥有传递延时任务的权利(见代码片段1)
    • 注意: 延时与非紧急任务有本质的区别,延时指的是startTime落后于当前时间,也就说该任务不应该在当前时间被调度. 但是非紧急的任务指的是任务过期时间较晚(expirationTime),也许某个任务不需要延迟,但是它的优先级低,那么当任务队列拥挤时,我们就可以将其放在后面更新(React采取最小堆排序任务),之后形成task对象
// 代码片段1
  var currentTime = getCurrentTime();
     //设定开始时间 (延时调度)
  var startTime;
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }
     // 设定过期时间长度 (非紧急任务)
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;
      break; 
      ....
  }
    // 设定过期时间
  var expirationTime = startTime + timeout;
  
   // **🌹任务**
  var newTask = {
    id: taskIdCounter++,
    callback, // performConcurrentWorkOnRoot函数
    priorityLevel, // 调度优先级
    startTime,
    expirationTime,
    sortIndex: -1, // 注意这个参数,排序最小堆的参数
  };
  • 2、并根据它是否被“延时”决定推送到timeQueue(延时优先队列)还是taskQueue(任务优先队列)(不要担心,笔者后面会来说它们的关系).

    • 我们要知道的是taskQueue的任务是可以被执行的,timeQueue的任务是被延时的, taskQueue需要先被调度光才能执行timeQueue的任务.
    • timeQueue,笔者在上面说了startTime表示任务的开始时间,那么在js中做到延时调度的不就是setTimeOut嘛,没错,但是我们还要考虑一个问题就是如果多个延时任务队列入队,那么是否一起调度呢.
      • 答案是否定的,一个延时任务会在当前宏任务完成后被检查执行,如果一任务创建一定时那么管理难度一定是很大的,React很聪明的做了一个决策:只维护最早会被触发的任务,如果有新的任务进来并且该任务早于已经创建的定时器触发时机(最近的task.startTime),那么销毁该定时器,转而创建newTask.startTime的定时器(见片段2.1)
    // 片段2.1 (衔接代码片段1)
    if (startTime > currentTime) {
        newTask.sortIndex = startTime;
    
      // 该push不同数组push,会进行一次堆排序,将sortIndex最小的排在数组顶部
        push(timerQueue, newTask); 
       // 当任务队列为空且当前任务的开始时间最早时,我们需要取消当前的延迟队列,并创建一个更早的延时队列开始调度
        if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
          if (isHostTimeoutScheduled) {
            cancelHostTimeout();
          } else {
            // 表示当前定时器开启了
            isHostTimeoutScheduled = true;
          }
          // Schedule a timeout.
          requestHostTimeout(handleTimeout, startTime - currentTime);
        }
    }
    
    • taskQueue,当前执行队列,存储startTime<=now()的任务,也就是说如果本次调度没有延迟,直接放入taskQueue堆中,并且判断开启唯一一次循环调度workLoop(见片段3)
    function unstable_scheduleCallback(priorityLevel, callback, options) {
     // 代码片段1 ....
    
     // 代码片段2
    
         // 片段3
       else {
        newTask.sortIndex = expirationTime;
        push(taskQueue, newTask);
        if (!isHostCallbackScheduled && !isPerformingWork) {
          isHostCallbackScheduled = true;
          requestHostCallback(flushWork);
        }
      }
    
      return newTask;
    }
    
    • 相信到了现在您的困惑肯定过多了,接下来我们就来更细的讲解这两个队列的作用.
    • 笔者首先从timerQueue开始讲起,由浅入深的到调度.
      • timerQueue中存储了“延时”调度的任务,不过,其中存储的task和taskQueue的并没有本质的差别,不过是taskQueue的任务的startTime已经到了,而timerQueue的任务还没有到,那么我们就可以将timerQueue想象为一个过渡的堆,当currentTime超过startTime就和taskQueue一样的方式进行调度.
    // 执行方式
    requestHostTimeout(handleTimeout, startTime - currentTime);
    
    // 这就是笔者上述说的延时调度
    function requestHostTimeout(callback, ms) {
      taskTimeoutID = localSetTimeout(() => { // setTimeOut
        callback(getCurrentTime());
      }, ms);
    }
        // callback函数
    function handleTimeout(currentTime) {
      // 定时器关闭,
      isHostTimeoutScheduled = false;
      // 检查到期任务,如果到期放入taskQueue中
      advanceTimers(currentTime);
      // 判断flushWork是否开始执行,如果执行,直接退出.
      if (!isHostCallbackScheduled) {
        // 定时器的任务被放在taskQueue中调度
        if (peek(taskQueue) !== null) {
          isHostCallbackScheduled = true;
          requestHostCallback(flushWork);
        } else {
          // 开启下一个延时任务
          const firstTimer = peek(timerQueue);
          if (firstTimer !== null) {
            requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
          }
        }
      }
    }
    function advanceTimers(currentTime) {
      // 最小对性质,取出startTime最小的,也就是最近将别调度的task
      let timer = peek(timerQueue);
      while (timer !== null) {
        //任务已结束
        if (timer.callback === null) {
          // Timer was cancelled.
          pop(timerQueue);
        } else if (timer.startTime <= currentTime) {
          // 已经到了可以执行的标准了,下一步就是进入优先级的比较了
          pop(timerQueue);
          // 以过期时间(优先级)为比较标准,放入taskQueue
          timer.sortIndex = timer.expirationTime;
          push(taskQueue, timer);
        } else {
        // 最小堆性质,一旦最近的任务开始时间晚于当前时间,后续不必要比较
          return;
        }
        timer = peek(timerQueue);
      }
    }
    
    • 现在我们知道了,对于timerQueue而言,不过是将到时的task放入taskQueue中,并且只有当taskQueue没有处于调度状态下才requestHostCallback或开启下一个定时器.
    • 接下来我们进入real任务调度,rsequestHostCallback(flushWork),🌹🌹🌹重点哦.
  • 首先React会依据宿主环境(window,jsdom,node)以此判断(setImmediate,messageChannel,setTimeOut)的方式为schedulePerformWorkUntilDeadline赋值(在本文开篇的时候笔者有说到原因->点这里.

// 调度任务
// rsequestHostCallback(flushWork)

function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}
const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // 全局定义的开始时间(后面有大用处)
    startTime = currentTime;
    const hasTimeRemaining = true;

    let hasMoreWork = true;
    try {
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      if (hasMoreWork) {
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
  // 直接退出当前workLoop
    isMessageLoopRunning = false;
  }
  needsPaint = false;
};
  • 综上,可以知道的是,performWorkUntilDeadline开始调度我们的task,并且会根据scheduledHostCallback的返回值,决定是否递归调度当前函数,那么我们就可以猜测调度的返回表示TaskQueue是否为空,从而决定递归发起调度.下面来印证我们的分析.
  • flushWork
    • 该函数触发了workLoop,并且取消了timeQueue的任务(这和我们上面看到的,这和我们上面看到的对已经存在scheduleTask,仅将延时任务放入timeQueue,并不生成定时器),这是React保证单一任务调度,不引发冲突的手段
    function flushWork(hasTimeRemaining, initialTime) {
      isHostCallbackScheduled = false;
      // 取消延时队列,防止冲突.
      if (isHostTimeoutScheduled) {
        // We scheduled a timeout but it's no longer needed. Cancel it.
        isHostTimeoutScheduled = false;
        cancelHostTimeout();
      }
    
      isPerformingWork = true;
      const previousPriorityLevel = currentPriorityLevel;
      try {
          return workLoop(hasTimeRemaining, initialTime);
      } finally {
        currentTask = null;
        currentPriorityLevel = previousPriorityLevel;
        isPerformingWork = false;
      }
    }
    
  • 「workLoop」
    • 在该函数中可以解答我们大部分的调度疑惑,并且还引申出了您应该有所耳闻的 「时间分片」.
    • 1、我们在workLoop中再一次执行了advanceTimers(currentTime),这里说出您可能有的困惑,在上文中我们提到对于延时触发的task(handleTimeout函数),我们就会执行一次advanceTimers,那么这次的执行的必要性就是.
      • 本次调度是以一个新的宏任务执行,而浏览器是单线程的,这就可能存在当执行当前workLoop时,有延时任务已经到期的,所以我们需要再做一次执行
    • 2、时间分片(下文中用🌹标记)
      • 我们常看到React的单帧5ms问题,下面让我们看看React是如何实现的.
      • 下面的函数用于判断是否应该继续执行,我们总共有两处会执行该函数.
        • 1、workLoopConcurrent中,对于每个Fiber执行完单元变化,我们就循环暂停.
        • 2、在开始调度时(workLoop中的循环体),我们也会判断.
      • 下面我们再来看看该函数中返回的判断.
        • 1、对于5ms未到的,不暂停.
        • 2、判断isInputPending(判断是否有输入事件).
          • 1、不支持该API,直接暂停.
          • 2、判断是否需要重绘.
            • 1、对于finishConcurrentRender,新的Fibertree渲染完成,并且有输入事件时,设置needPaint = true;执行重绘.(commitImpl中执行)
             function requestPaint() {
                  if (
                    enableIsInputPending &&
                    navigator !== undefined &&
                    navigator.scheduling !== undefined &&
                    navigator.scheduling.isInputPending !== undefined
                  ) {
                    needsPaint = true;
                  }
                }
            
            • 2、对于未到浏览器的持续事件间隔,我们不暂停.
            • 3、对于超过最大间隔的,我们根据是否有输入事件,决定是否让出主线程.
      function shouldYieldToHost() {
        const timeElapsed = getCurrentTime() - startTime;
        // 给5ms时间,如果没到允许执行.
        if (timeElapsed < frameInterval) {
          return false;
        }
        if (enableIsInputPending) {
          // 需要重绘直接退出,暂停
          if (needsPaint) {
            return true;
          }
          // 对于当前过期时间比持续输入间隔短的
          if (timeElapsed < continuousInputInterval) {
            // 暂停离散事件
            if (isInputPending !== null) {
              return isInputPending();
            }
          } else if (timeElapsed < maxInterval) {
            // 对于离散和持续输入直接暂停
            if (isInputPending !== null) {
              return isInputPending(continuousOptions);
            }
          } else {
            return true;
          }
        }
        return true;
      }
      
    • 在循环中,我们执行了performConcurrentWorkOnRoot,我们会依赖第二个参数didTimeOut(下文中callback传入的参数,因为我们之前放入该任务时已经绑定了第一个参数root,所以执行时的传入会作为第二个参数).从而确定任务紧急程度决定是否分片.
        // performConcurrentWorkOnRoot中的代码片段
        const shouldTimeSlice =
          !includesBlockingLane(root, lanes) &&
          !includesExpiredLane(root, lanes) &&
          (disableSchedulerTimeoutInWorkLoop || !didTimeout);
        let exitStatus = shouldTimeSlice
          ? renderRootConcurrent(root, lanes)
          : renderRootSync(root, lanes);
      
    function workLoop(hasTimeRemaining, initialTime) {
      let currentTime = initialTime;
      // 当前时间下需要执行的任务,被放入到taskQueue中
      advanceTimers(currentTime);
      // 根据执行时机循环
      currentTask = peek(taskQueue);
      while (
        currentTask !== null &&
        !(enableSchedulerDebugging && isSchedulerPaused)
      ) {
        if (
          currentTask.expirationTime > currentTime &&
          (!hasTimeRemaining || shouldYieldToHost())
        ) {
          // 时间分片🌹
          break;
        }
        const callback = currentTask.callback;
        if (typeof callback === 'function') {
          currentTask.callback = null;
          currentPriorityLevel = currentTask.priorityLevel;
          // 任务是否过期
          const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
          // 判断过期执行performConcurrentWorkOnRoot或sync
          const continuationCallback = callback(didUserCallbackTimeout);
          currentTime = getCurrentTime();
          // 任务被中断,下次继续执行该任务
          if (typeof continuationCallback === 'function') {
            currentTask.callback = continuationCallback;
          } else {
            // 弹出当前任务
            if (currentTask === peek(taskQueue)) {
              pop(taskQueue);
            }
          }
          // 放入过期任务
          advanceTimers(currentTime);
        } else {
          pop(taskQueue);
        }
        // 从最紧急的开始循环
        currentTask = peek(taskQueue);
      }
      // Return whether there's additional work
      if (currentTask !== null) {
        // 当前任务未到期,继续循环,让出主线程.
        return true;
      } else {
        // 判断队头,如果存在延时任务,那么延时调度,并结束本次loop
        const firstTimer = peek(timerQueue);
        if (firstTimer !== null) {
          requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
        }
        return false;
      }
    }
    
  • 总结:
    • 1、对于紧急任务,React会放入syncQueue,并创建微任务,异步执行
    • 2、对于非紧急任务,React将未到开始时间的任务(task.start < Date.now)放入timerQueue(延时最小堆),将可以执行的任务以过期时间作为排序参数放入TaskQueue(任务最小堆).
      • 根据是否存在taskSchedule决定是否创建定时器(定时器以最近的开始时间为延时时间,调度TaskSchedule)
      • 在Taskshcedule中会将timeQueue到期的取出至TaskQueue,然后workLoop该taskQueue,并在循环中进行时间分片(一个任务开始调度、一个workInprogress创建完毕),对于暂停的一次任务调度而言,并不将它从任务队列中取出,而是创建下一个channelMessage宏任务,在渲染任务执行完成后继续执行.

好了,我们现在已经讲完了任务的调度了,下面我们看一个很经典的任务调度的题目,笔者认为可以完美回答下面问题的人,必定是对React源码有深度思考的人.

  • 题目与答案已经写在下面了(这里之所以用ClassComponent组件而不用Hook,是因为引用类型的指向问题),不过能够完美回答该题目的,笔者可以说已经干掉99%的前端er了.
// API-1
// ReactDOM.createRoot(document.getElementById('root')).render(
//   <React.StrictMode>
//     <App />
//   </React.StrictMode>);

// API-2
ReactDOM.render(<App />,document.getElementById('root'));

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      name: 123
    }
  }
  render() {
    return <div onClick={() => {
      setTimeout(() => {
        this.setState({name: 1});
        console.log(this.state.name); // (API-1。123) (API-2. 1)
        this.setState({name: 2});
        console.log(this.state.name);// (API-1。123) (API-2. 2)
      }, 0);
    }}>123</div>
  }
}
  • 分析:
    • 1、对于rendercreateRoot,React分别赋予了NoMode和concurrentMode.我们也在上面提到过,render下面触发的更新React都以紧急任务调度
    • 2、上文我们提到过,setState会触发scheduleUpdateOnFiber、ensureRootIsScheduled函数,笔者上面都提到过,API-2之所以会产生该现象的是原因是我们在scheduleUpdateOnFiber中有判断一次executionContext,通过该全局变量确定是否在React环境,对于setTimeOutrender模式下,会将紧急任务直接同步更新flushSyncCallbacksOnlyInLegacyMode,该函数的执行在batchUpdate下(事件结束时)也存在,这种更新就是我们所知道的 「批量更新」.
    • 对于API-1而言,由于存在concurrent模式,所以所有的调度都是存在优先级的.那么产生该现象的原因其实有两个.
      • 1、在setTimeOut环境下,我们拿到的updateLane是defaultPriority(16),并不是sync(1).即使和render mode相同调度,依然不会进入syncQueue,所以并不会同步执行.
      • 2、在ensureRootIsScheduled中,我们将非紧急任务以ChannelMessage的形式异步调度.

笔者拿出该题的目的是该题较为经典,但是鲜有文章真的看到该题的本质.如果您以笔者的思路去深入题目本质,那么这就是您和其他前端er不同的地方.


到此,事件调度告一段落了,现在我们对事件、优先级、车道等概念已经有了比较深刻的概念,不过笔者却是没有非常详细的对其中车道的位运算进行深入分析.主要是场景难复现,资料又有限.总的来说,这一块的代码还是稍有难度的,但同时也可以说是能React的核心代码,希望本篇对您有帮助


--ReactV17+下的事件机制--

笔者在complete的过程中花了大量的事件去思考事件机制,不的不说这一块的资料真心不多,我们进入正题.

  • React为了实现多平台API的统一性,实现了自己的一套事件驱动机制.它将所有事件的驱动都放到了自己的顶层节点上,以观察者模式实现内容分发.
  • React17相比于React16的差别就是事件被绑定到了RootContainer上,而不是document上,这么做的好处是避免了页面中多个React应用的引起的事件冲突.
  • 接下来我们从React.render上的主函数的事件监听入口开始探索源码
    • 前置数据.
    • 首先我们进入到该文件内/react-dom/src/events/DOMPluginEventSystem.ts,该文件是事件调度的核心文件.

    下面被横线以内的数据可以暂时跳过,在后面提到时回头看.


    • // 注册ReactName->domEventName的映射对象
      SimpleEventPlugin.registerEvents(); // 注册普通事件
      EnterLeaveEventPlugin.registerEvents(); // 鼠标移动事件, onMouseEnter.
      ChangeEventPlugin.registerEvents(); // 状态改变事件,onChange
      SelectEventPlugin.registerEvents(); // 注册onSelect
      BeforeInputEventPlugin.registerEvents(); // 注册输入之前事件,不常使用
      
    • 这里的每一个rigisterEvents对应一个文件那的事件注册,接下来我们进入simpleEventPlugin来窥探一下内部实现.
    • const simpleEventPluginEvents = [ // 原生事件数组    'abort',    'auxClick',    'cancel',2    'canPlay',    'canPlayThrough',    'click',   ];
      
      // 注册函数
      export function registerSimpleEvents() {
      for (let i = 0; i < simpleEventPluginEvents.length; i++) {
        // 这里我们在创建原生事件和React事件,并进入真正的注册函数.
        const eventName = ((simpleEventPluginEvents[i]: any): string);
        const domEventName = ((eventName.toLowerCase(): any): DOMEventName);
        const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);
        registerSimpleEvent(domEventName, 'on' + capitalizedEvent);
      }
      // Special cases where event names don't match.
      registerSimpleEvent(ANIMATION_END, 'onAnimationEnd');
      registerSimpleEvent(ANIMATION_ITERATION, 'onAnimationIteration');
      registerSimpleEvent(ANIMATION_START, 'onAnimationStart');
      registerSimpleEvent('dblclick', 'onDoubleClick');
      registerSimpleEvent('focusin', 'onFocus');
      registerSimpleEvent('focusout', 'onBlur');
      registerSimpleEvent(TRANSITION_END, 'onTransitionEnd');
      }
      
      
      // 进入
      function registerSimpleEvent(domEventName, reactName) {
        // map注册当前dom下的事件.
        topLevelEventsToReactNames.set(domEventName, reactName);
        // 事件注册
        registerTwoPhaseEvent(reactName, [domEventName]);
      }
      
       
      // export function registerTwoPhaseEvent(
          registrationName: string,
          dependencies: Array<DOMEventName>,
        ): void {
            // 这里我们可以看到同时注册了捕获和冒泡事件.
          registerDirectEvent(registrationName, dependencies);
          registerDirectEvent(registrationName + 'Capture', dependencies);
        }
        
        export function registerDirectEvent(
          registrationName: string,
          dependencies: Array<DOMEventName>,
        ) {
          // 这里是ReactName-> domEventName的映射表
          registrationNameDependencies[registrationName] = dependencies;
          // 这里我们存储了所有原生事件
          for (let i = 0; i < dependencies.length; i++) {
            allNativeEvents.add(dependencies[i]);
          }
        }
      

    • listenToAllSupportedEvents
      • 该函数是监听所有被支持事件的入口.(V17的做出的变化)
      • 该函数会将所有原生事件绑定到rootContianer上,统一控制事件分发.
      • export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
        // 判断当前节点是否开启监听状态
        if (!(rootContainerElement: any)[listeningMarker]) {
          (rootContainerElement: any)[listeningMarker] = true;
          // 遍历所有原生事件监听列表,这里可以我们可以回头查看我们之前说到的.
          allNativeEvents.forEach(domEventName => {
            if (domEventName !== 'selectionchange') {
              // 对于不会冒泡的事件不绑定冒泡监听
              if (!nonDelegatedEvents.has(domEventName)) {
                // 重点函数
                listenToNativeEvent(domEventName, false, rootContainerElement);
              }
              // 绑定捕获监听.
              listenToNativeEvent(domEventName, true, rootContainerElement);
            }
          });
          const ownerDocument =
            (rootContainerElement: any).nodeType === DOCUMENT_NODE
              ? rootContainerElement
              : (rootContainerElement: any).ownerDocument;
          if (ownerDocument !== null) {
            if (!(ownerDocument: any)[listeningMarker]) {
              (ownerDocument: any)[listeningMarker] = true
              // 该事件只能注册到document上
              listenToNativeEvent('selectionchange', false, ownerDocument);
            }
          }
        }
        } 
        
    • listenToNativeEvent
      • 该函数实现事件注册到目标节点.
      • export function listenToNativeEvent(
          domEventName: DOMEventName,
          isCapturePhaseListener: boolean,
          target: EventTarget,
        ): void {
         let eventSystemFlags = 0;
         if (isCapturePhaseListener) {
         // 标记捕获
           eventSystemFlags |= IS_CAPTURE_PHASE;
         }
         // 再次进入
         addTrappedEventListener(
           target,
           domEventName,
           eventSystemFlags,
           isCapturePhaseListener,
         );
        }
        
    • addTrappedEventListener(事件重点)
      • 根据事件优先级创建回调并并注册
      • function addTrappedEventListener(
            targetContainer: EventTarget,
            domEventName: DOMEventName,
            eventSystemFlags: EventSystemFlags,
            isCapturePhaseListener: boolean,
            isDeferredListenerForLegacyFBSupport?: boolean,
          ) {
          // 创建回调,这里我们重点关注一下.
            let listener = createEventListenerWrapperWithPriority(
              targetContainer,
              domEventName,
              eventSystemFlags,
            );
            // 对于部分可能会由于持续触发而占用主线程的进行判定
            // 因为浏览器无法知道回调是否存在preventDefault而进行滚动
            // 所有会等待回调执行,那么就可能会引起掉帧.
            // 该参数就就是告诉浏览器,我的回调里不存在preventDefault.
            let isPassiveListener = undefined;
            if (passiveBrowserEventsSupported) {
              if (
                domEventName === 'touchstart' ||
                domEventName === 'touchmove' ||
                domEventName === 'wheel'
              ) {
                isPassiveListener = true;
              }
            }
        
            let unsubscribeListener;
        
            // 设置监听事件addEventListener
            // 捕获以及isPsssive判定和回调绑定
            if (isCapturePhaseListener) {
              if (isPassiveListener !== undefined) {
                // 下列的add函数就是进入事件监听,我们不下去看了.
                unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
                  targetContainer,
                  domEventName,
                  listener,
                  isPassiveListener,
                );
              } else {
                unsubscribeListener = addEventCaptureListener(
                  targetContainer,
                  domEventName,
                  listener,
                );
              }
            } else {
              if (isPassiveListener !== undefined) {
                unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
                  targetContainer,
                  domEventName,
                  listener,
                  isPassiveListener,
                );
              } else {
                unsubscribeListener = addEventBubbleListener(
                  targetContainer,
                  domEventName,
                  listener,
                );
              }
            }
          }
        
    • createEventListenerWrapperWithPriority, 判定事件优先级,创建监听回调.
      • React将事件分为离散事件、持续事件、默认事件优先级.『执行顺序从高到低』
      •   export function createEventListenerWrapperWithPriority(
            targetContainer: EventTarget,
            domEventName: DOMEventName,
            eventSystemFlags: EventSystemFlags,
          ): Function {
            // 根据事件名获取优先级
            const eventPriority = getEventPriority(domEventName);
            let listenerWrapper;
            // 根据事件优先级设置不同的事件,让他们入不同事件优先级的队列.
            switch (eventPriority) {
              case DiscreteEventPriority:
                listenerWrapper = dispatchDiscreteEvent;
                break;
              case ContinuousEventPriority:
                listenerWrapper = dispatchContinuousEvent;
                break;
              case DefaultEventPriority:
              default:
                listenerWrapper = dispatchEvent;
                break;
            }
            // 创建事件分发函数, 套在事件监听器之中, 让React决定调度
            return listenerWrapper.bind(
              null,
              domEventName,
              eventSystemFlags,
              targetContainer,
            );
          }
        
    • dispatchEventsForPlugins,这里我们跳过中间的一系列处理过程,进入关键调度.
      • 该函数会进行合成事件创建事件冒泡执行等操作.
      • -> function dispatchEventsForPlugins( domEventName: DOMEventName, // 原生事件名 eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, // 原生事件参数e targetInst: null | Fiber, // 目标Fiber targetContainer: EventTarget, // currentTarget ): void { // 获取currentTarget const nativeEventTarget = getEventTarget(nativeEvent); const dispatchQueue: DispatchQueue = []; // dispatchQueue中放置对应事件名的事件列表 // 该函数进入分发队列进行赋值 // 在该函数中,我们首先会switch(domEventName)合成不同事件,使多平台事件标准化 // 然后我们会根据targetInst(目标Fiber)回溯Fiber树-> // 1、从而获取dom树并将其Dom指向合成事件的currentTarget并在调度中作为参数传入, // 2、获取所有上层Fiber的props中和当前ReactEventName相符的回调并传入DispatchEvents中. // 3、DisPatchEvents中从头到尾是子到父的排列.(后续有用) extractEvents( dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer, ); // 该队列的每一个对象表示由该事件触发「冒泡或捕获」的所有回调函数列表 // Queue [{instance: null,listeners: callbackArr,event: SyntheticFocusEvent}]; // 该函数,会对event的是否阻止冒泡以及捕获等进行判断,然后遍历分发列表进行调度. // 冒泡,从头到尾执行, 捕获, 从尾到头执行. processDispatchQueue(dispatchQueue, eventSystemFlags); }

笔者这里空出了processDispatchQueueextractEvents以及合成事件函数希望您能自行翻译,主要原因是这些内容嵌套较多,而且比较核心,难度也并不高,希望您主动阅读.


  • 下面我们捋一下全过程
  • 1、首先在ReactEvent文件中我们注册了domEventName->ReactEventName的映射、allNativeEvents等前置数据
  • 2、遍历allNativeEvents并对于相应原生事件的冒泡(特殊不冒泡做相应处理)和捕获两状态设置回调(见下一步),并注册监听器到RootContainer
  • 3、根据ReactEventName获取相应事件优先级,进入不同调度队列
  • 5、根据原生e.targetEvent(兼容性这里不多写了),拿到internalInstanceKey属性上的Fiber映射
  • 6、回溯Fiber树,拿到所有祖先层Fiber的props和stateNode(对应dom)
  • 7、合成listeners, 例listeners: [{listener: Fn1, currentTarget: Fn1被放置的dom}]
  • 4、根据当前ReactEventName,是否冒泡,合成事件SyntheticEvent
  • 5、合成dispatchEvents存储所有事件调度, 例[{event: SyEvent1, listeners: ls1}]
  • 6、遍历dispatchEvents,其中每一个合成事件对应多个回调,遍历listeners,执行回调并将currentTarget赋给e作为回调参数默认传入然后执行

总的来说事件这一块还是有一定挑战,原因是V17将合成事件进行了重写,导致网上资料较少,并且前置条件和嵌套多,笔者并没有将关注点放在合成事件以外的地方(自己也有地方不熟悉😓) 但是也算是打开了笔者的源码大门,冲!!!

参考资料: