Code Monkey home page Code Monkey logo

react-study's Introduction

react-study's People

Contributors

astak16 avatar

Stargazers

 avatar

Watchers

 avatar

react-study's Issues

useEffect 返回的函数是怎么执行的

掌握 React 组件树遍历技巧中说到 react 是怎么遍历 dom

那么在遍历的过程中,如果发现当前节点有子节点被删除了,那么 react 会怎么处理呢?

下面是源码简化:这里是完整的源码

function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void {
  const deletions = parentFiber.deletions;
  if ((parentFiber.flags & ChildDeletion) !== NoFlags) {
    if (deletions !== null) {
      for (let i = 0; i < deletions.length; i++) {
        const childToDelete = deletions[i];
        nextEffect = childToDelete;
        commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
          childToDelete,
          parentFiber
        );
      }
    }
  }
}
function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
  deletedSubtreeRoot: Fiber,
  nearestMountedAncestor: Fiber | null
) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    // 执行 passive effects 返回的函数
    commitPassiveUnmountInsideDeletedTreeOnFiber(fiber, nearestMountedAncestor);

    const child = fiber.child;
    if (child !== null) {
      child.return = fiber;
      nextEffect = child;
    } else {
      commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
        deletedSubtreeRoot
      );
    }
  }
}

function commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
  deletedSubtreeRoot
) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    const sibling = fiber.sibling;
    const returnFiber = fiber.return;

    if (fiber === deletedSubtreeRoot) {
      nextEffect = null;
      return;
    }

    if (sibling !== null) {
      sibling.return = returnFiber;
      nextEffect = sibling;
      return;
    }

    nextEffect = returnFiber;
  }
}

deletions

在正式开始之前,我们要了解一个 fiber 的属性:deletions

这个属性存放的是当前节点中被删除的 fiber,这个数组是在 commit 阶段被赋值的

如果有被删除的节点,这个属性值是一个数组,如果没有被删除的节点,这个属性值是 null

const A = () => {
  useEffect(() => {
    return () => {
      console.log("A unmount");
    };
  }, []);
  return <div>文本A</div>;
};
const B = () => {
  useEffect(() => {
    return () => {
      console.log("B unmount");
    };
  }, []);
  return <div>文本B</div>;
};

如果 App 组件这样写,那么 deletions 的值是 [FiberNode, FiberNode]

const App(){
  const [count, setCount] = useState(0)

  return <div>
    {count % 2 === 0 && <A />}
    {count % 2 === 0 && <B />}
    <div onClick={()=> setCount(count+1)}>+1</div>
  </div>
}

如果 App 组件这样写,那么 deletions 的值是 [FiberNode]

const App(){
  const [count, setCount] = useState(0)

  return <div>
    {count % 2 === 0 && <><A /><B /></>}
    <div onClick={()=> setCount(count+1)}>+1</div>
  </div>
}

对于第二种情况,react 会把 A 组件和 B 组件作为一个整体,所以 deletions 的值是 [FiberNode]

处理当前节点的 deletions

react 在遍历 fiber tree 时,会先处理当前的 fiberdeletions,等处理完之后再遍历下一个 fiber

现在我们已经知道 deletions 中保存的是当前 fiber 下被删除的子节点

这时 react 会遍历 deletions 数组,然后执行每个 fiberpassive effect 返回的函数

但是有个问题,如果 deletions 中的 fiber 有子节点,那么这些子节点也会被删除,这时 react 会怎么处理呢?

这里分两种情况来讨论:

  1. 删除的 fiber 没有子节点:<div>{xxxx && <A />}</div>
  2. 删除的 fiber 有子节点:<div>{xxxx && <><A /><B /></>}</div> -->

删除的 fiber 没有子节点:<div>{xxxx && <A />}</div>

这种情况比较好理解

当遍历到 div 时,因为 <A/> 节点会被卸载,所以在 divdeletions 保存了一个 <A/>fiber

遍历 deletions 数组,执行 <A/>passive effect 返回的函数

如下图所示:

删除的fiber没有子节点

删除的 fiber 有子节点:<div>{xxxx && <><A /><B /></>}</div>

这种情况就比较复杂了

当遍历到 div 时,<></> 节点会被卸载,所以在 divdeletions 保存了一个 <></>fiber

遍历 deletions 数组,执行 fiberpassive effect 返回的函数,对于 <></> 来说是不存在的 passive effect

那么这个时候就要去遍历它的 child.fiber,也就是 <A/><B/>

首先拿到第一个 fiber,也就是 <A/>,然后执行 <A/>passive effect 返回的函数,这步比较好理解

child = fiber.child;
if (child !== null) {
  nextEffect = child;
}

这里遍历也是深度优先,遍历一个 child,执行一个 passive effect 返回函数,然后再遍历下一个 child(这边 <A /> 已经是叶子节点了)

然后拿到第二个 fiber,也就是 <B/>,然后执行 <B/>passive effect 返回的函数,这步就不太好理解了

child = fiber.child;
if (child !== null) {
  nextEffect = child;
} else {
  commitPassiveUnmountEffectsInsideOfDeletedTree_complete(deletedSubtreeRoot);
}

这里要注意的是:

react 在寻找有 passive effectfiber 时,只遍历到有 passive effectfiber, 像 div 这种没有 passive effect 就不会遍历

但是在处理 deletionsreact 会遍历所有的 fiber,也就是说从当前的 fiber 开始,一直往下遍历到叶子节点,这个叶子节点是指文本节点这种,往下不会有节点了(对于 A 组件来说 文本A 是文本节点)

然后在开始往上遍历,往上遍历是调用 commitPassiveUnmountEffectsInsideOfDeletedTree_complete 函数,直到遍历到 deletionRoot,在向上遍历的过程中会检查是否有 sibling,如果有说明 sibling 还没被处理,这样就找到了 <B/>,然后执行 <B/>passive effect 返回的函数

如下图所示:

删除的fiber有子节点

向下遍历和向上遍历

在处理 deletions 时,对于每个 deletedNode,都先向下遍历,然后再向上遍历

  • 向下遍历:commitPassiveUnmountEffectsInsideOfDeletedTree_begin(深度优先,优先处理左边的节点)
  • 向上遍历:commitPassiveUnmountEffectsInsideOfDeletedTree_complete(之后再处理右边节点)

总结

1. 遍历 deletions 数组:

  • react 在处理 deletions 时,先沿着 fiber tree 向下遍历,如果有 passive effect 返回的函数,则执行
  • 一直遍历到没有 childfiber,再向上遍历,处理 sibling
  • 再向上遍历时,如果如果遇到 sibling,再向下遍历,向下遍历时遇到 passive effect 返回的函数,则执行
  • 如此循环直到遍历到 deletedNode,结束遍历

2. 结合掌握 React 组件树遍历技巧

  • 遍历寻找有 passive effect 节点
    • react 从根组件向下遍历,如果没有 passive effect,则不会遍历
  • 遍历时,如果遇到当前节点有 deletions 时,会暂停寻找 passive effect 节点
    • 进入遍历 deletions 数组

react 遍历 deletions 完整逻辑如下图所示:

图中绿色部分是遍历 deletionsNode 过程,红色部分是遍历寻找 passive effect 过程

react执行deletions逻辑

React Lane 算法:一文详解 8 种 Lane 操作

什么是 lane

lanereact@17 中用于表示任务的优先级,是对 expirationTime 的重构

lane 是一个 32 位的二进制数,每个二进制位表示 1 种优先级,优先级最高的 SyncLane1,其次为 248

lanes 是一个整数,该整数所有为二进制位为 1 对应的优先级任务都将被执行

注意:lane 的长度是 31 位,react 这么做是为了避免符号位参与运算

reactlane 的操作有 8 种:

  • lane & lane
  • lane & lanes
  • lanes & ~lane
  • lanes1 & lanes2
  • lane | lane
  • lanes2 |= lanes1 & lane
  • lane *= 2lane <<= 1
  • lanes & -lanes

在下面将会举例详细介绍这些操作,这里先介绍一下 lane 的值:

lanes 值为 0b0000000011111111111111110000000,表示有多个任务的优先级。

TransitionLane1 值为 0b0000000000000000000000010000000,表示单个任务的优先级

TransitionLane2 值为 0b0000000000000000000000100000000,表示单个任务的优先级

lane & lane

用来判断是不是同一个 lane,两个 lane 是否有相同的位为 1(取交集)

比如:lane & TransitionLane1,如果 lane 的值为 0b0000000000000000000000010000000,则输出 0b0000000000000000000000010000000,否则输出 0

用于 getLabelForLane 函数

lane & lanes

用来判断 lanes 中是否有 lane,是否有相同的位为 1(取交集)

如果想判断 lanes 中是否有 lane,进行如下计算:

TransitionLane1lanes 进行按位与,得到 lane & lanes,它的值是 0b0000000000000000000000010000000,和 TransitionLane1 值相同,说明 lanes 中有 TransitionLane1 任务

用于 isTransitionLane 等函数

lanes & ~lane

用来从 lanes 中删除 lane(取差集)

如果想去从 lanes 中删掉 lane,具体步骤如下:

  1. TransitionLane1 取反,得到 ~lane,即 0b1111111111111111111111101111111
  2. lanes~lane 进行按位与运算,得到 lanes & ~lane,即 0b0000000011111111111111100000000
  3. 这样就把 lanes 中的 TransitionLane1 置为了 0,也就是去掉了这个任务
  4. 如果 lane 不在 lanes 中,那么 lanes & ~lane 的值就是 lanes,即 0b0000000011111111111111110000000

用于 getNextLanes 等函数

lanes1 & lanes2

用于判断 lanes1 中是否有 lane 属于 lanes2(取交集)

如果想判断 lanes1 中是否有 lane 属于 lanes2,进行如下计算:

  1. 假设 lanes2SyncDefaultLanes,它是由 InputContinuousHydrationLane | InputContinuousLane | DefaultHydrationLane | DefaultLane 组成的,即 0b0000000000000000000000000111100
  2. lanes13 ~ 6 位为 1,即 lanes10b0000000000000000000000000111100
  3. lanes1 & lanes2 的值为 lanes1,即 0b0000000000000000000000000111100,说明 lanes1 中有 lanes2 中的 lane
  4. 如果 lanes1 中没有 lane 属性 lanes2,那么 lanes1 & lanes2 的值为 0

这种用法有种变形:lanes & (lane | lane)

用于 includesNonIdleWorkincludesSyncLane 等函数

lane | lane

用于将多个 lane 合并为一个 lanes(取并集)

合并两个 laneTransitionLane1 | TransitionLane2,得到的值为 0b0000000000000000000000110000000

用于 markHiddenUpdate 等函数

lanes2 |= lanes1 & lane

用于将 lanes1 中的 lane 合并到 lanes2 中(先取交集,再取并集)

这种写法等于:lanes2 = lanes2 | (lanes1 & lane)

如果想从 lanes1 中取出 lane,并将它合并到 lanes2 中,进行如下计算:

  1. lanes1InputContinuousHydrationLane | InputContinuousLane,即 0b0000000000000000000000000001100
  2. lanes2DefaultHydrationLane | DefaultLane,即 0b0000000000000000000000000110000
  3. laneInputContinuousLane,即 0b0000000000000000000000000001000
  4. lanes1 & lane 的值为 InputContinuousLane,即 0b0000000000000000000000000001000
  5. lanes2 |= lanes1 & lane 的值为 DefaultHydrationLane | DefaultLane | InputContinuousLane,即 0b0000000000000000000000000111100
  6. lanes2 中多了 InputContinuousLane 这个任务

用于 markRootMutableReadmarkRootPinged 等函数

lane *= 2 和 lane <<= 1

都是将 lane 左移一位,一般来说位运算比乘法运算快

TransitionLane1 *= 2TransitionLane1 <<= 1 的结果都是 0b0000000000000000000000100000000

用于 getLaneLabelMapclaimNextRetryLane 等函数

lanes & -lanes

lanes 中找出最高优先级的 lane

如果想找出 lanes 中最高优先级的 lane,进行如下计算:

  1. lanes 取反,得到 ~lanes,即 0b1111111100000000000000001111111
  2. 末尾加 1,得到 -lanes,即 0b1111111100000000000000010000000
  3. lanes-lanes 进行按位与运算,得到 lanes & -lanes,即 0b0000000000000000000000010000000
  4. 这样就找出了 lanes 中最高优先级的 lane

用于 getHighestPriorityLane 函数

补充说明:

下面二进制数都是 32 位带符号位二进制数

~ 是按位取反

十进制数 4,按位取反是 -5,记做 ~4,计算逻辑如下:

  1. 将十进制数 4 转换为二进制数为 0b00000000000000000000000000000100
  2. 按位取反,即将 1 变为 0,将 0 变为 1,得到 0b11111111111111111111111111111011
  3. 符号位不变,其他位取反,得到 0b10000000000000000000000000000100
  4. 末位加 1,得到 0b10000000000000000000000000000101
  5. 将二进制数转换为十进制数,得到 -5

js 中按取反

js 中按位取反只是一个按位取反,并不表示按位取反后的数是实际的负数

  1. 十进制整数 4,转换为二进制数为 0b00000000000000000000000000000100
  2. 按位取反,即将 1 变为 0,将 0 变为 1,得到 0b11111111111111111111111111111011

4 & ~4 结果是 0

因为 4 的二进制数为 0b00000000000000000000000000000100~4 的二进制数为 0b11111111111111111111111111111011,不是 0b10000000000000000000000000000101

也就是说 ~4 的二进制数是 4 的二进制数取反

js 中的负数

十进制整数 14,负数为 -14

14 & -14 结果是 2

为什么结果会是 2 呢?

因为 14 的二进制数为 0b00000000000000000000000000001110-14 的二进制数为 0b11111111111111111111111111110010,不是 0b10000000000000000000000000001110

也就是说,-14 的二进制数是 14 的二进制数取反后再加 1

最后

  1. js 中对于二进制数操作要特别小心:~ 是按位取反(末尾不加一),- 取反末尾加一
  2. -lane === (~lane + 1)

总结

  1. lane & lane:用来判断是不是同一个 lane(是否有相同的位为 1,取交集)
  2. lane & lanes:用来判断 lanes 中是否有 lane(是否有相同的位为 1,取交集)
  3. lanes & ~lane:用来从 lanes 中删除 lane(取差集)
  4. lanes1 & lanes2:用于判断 lanes1 中是否有 lane 属于 lanes2(取交集)
  5. lane | lane:用于将多个 lane 合并为一个 lanes(取并集)
  6. lanes2 |= lanes1 & lane:用于将 lanes1 中的 lane 合并到 lanes2 中(先取交集,再取并集)
  7. lane *= 2lane <<= 1:都是将 lane 左移一位
  8. lanes & -lanes:从 lanes 中找出最高优先级的 lane

ReactDOM.createRoot 被调用时在做什么(代码片段)

ReactDOM.createRoot() 时会挂载事件,内部会调用 listenToAllSupportedEvents 函数

// rootContainerElement 是 <div id="root"></div>
listenToAllSupportedEvents(rootContainerElement);

listenToAllSupportedEvents

listenToAllSupportedEvents 内部具体做了什么,大致分为 3 步:

  1. listeningMarker 这变量是一个 36 位随机数,这里用来保证事件只注册一次

    // toString(radix) radix 是进制
    // 36 进制,则 0-9 用 0-9 表示,10-35 用 a-z 表示
    const listeningMarker =
      "_reactListening" + Math.random().toString(36).slice(2);
  2. 遍历所有的原生事件 allNativeEvents(如何处理原生事件名,在下面【原生事件收集一章中】)

    • react 将原生事件按照是否支持冒泡,分为:

      • allNativeEvents 支持冒泡
      • nonDelegatedEvents 不支持冒泡

      react 这么做的目的是因为,react 将所有的事件都绑定在根元素上,在 react@18 中根元素是 div#rootdom 元素,不支持冒泡的事件,就绑定在这个元素上

    • 单独处理 selectionchange 事件,将它绑定到 document

  3. listenToNativeEvent 是具体挂载事件的函数(具体在下面 【listenToNativeEvent 一章中】)

// rootContainerElement 是 <div id="root"></div>
function listenToAllSupportedEvents(rootContainerElement) {
  // 保证事件只注册一次
  if (!rootContainerElement[listeningMarker]) {
    rootContainerElement[listeningMarker] = true;
    // 遍历 set 集合 allNativeEvents,监听冒泡和捕获阶段
    allNativeEvents.forEach((domEventName) => {
      // We handle selectionchange separately because it
      // doesn't bubble and needs to be on the document.
      // 单独处理 selectionchange
      if (domEventName !== "selectionchange") {
        // delegate 委托,不需要委托的事件,这些事件不会冒泡
        if (!nonDelegatedEvents.has(domEventName)) {
          /**
           * @param domEventName - 事件名,如 click
           * @param isCapturePhaseListener- 是否是捕获阶段 false,非 nonDelegatedEvents
           * @param target - <div id="root"><div>
           */
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }
        /**
         * @param domEventName - 事件名,如 click
         * @param isCapturePhaseListener- 是否是捕获阶段 true,nonDelegatedEvents
         * @param target - <div id="root"><div>
         */
        listenToNativeEvent(domEventName, true, rootContainerElement);
      }
    });
    // 是否是 document 元素,ownerDocument 可以获取某个 dom 的 document 元素
    const ownerDocument =
      rootContainerElement.nodeType === DOCUMENT_NODE
        ? rootContainerElement
        : rootContainerElement.ownerDocument;
    if (ownerDocument !== null) {
      // The selectionchange event also needs deduplication
      // but it is attached to the document.
      if (!ownerDocument[listeningMarker]) {
        ownerDocument[listeningMarker] = true;
        listenToNativeEvent("selectionchange", false, ownerDocument);
      }
    }
  }
}

原生事件收集

react 把事件分为 5 类,分别由各事件插件进行处理:

  • SimpleEventPlugin 简单事件,代表事件 onClick
  • EnterLeaveEventPlugin 鼠标进出事件,代表事件 onMouseEnter
  • ChangeEventPlugin 表单修改事件,代表事件 onChange
  • SelectEventPlugin 选择事件,代表事件 onSelect
  • BeforeInputEventPlugin 输入前事件,代表事件 onBeforeInput

简单事件是指,只依赖自身的事件,比如:onClick 只依赖 click。而有些事件是依赖其他事件的,比如 onMouseEnter 依赖 ["mouseout", "mouseover"]react 这么做简化了开发者自己监听事件的步骤,不然开发者要实现 onMouseEnter 这种事件,开发者分别需要监听 mouseoutmouseover 事件

SimpleEventPlugin.registerEvents(); // click, cancel, input, keyUp => onClick, onCancel, onInput, onKeyup ...
EnterLeaveEventPlugin.registerEvents(); // mouseout, mouseover => onMouseEnter, onMouseLeave ...
ChangeEventPlugin.registerEvents(); // change, click, focusin, focusout, input, keydown, keyup, selectionchange => onChange
SelectEventPlugin.registerEvents(); // focusout, contextmenu, dragend, focusin, keydown, keyup, mousedown, mouseup, selectionchange => onSelect
BeforeInputEventPlugin.registerEvents(); // compositionend, keypress, textInput, paste => onBeforeInput ...

5 个插件内部都调用了 registerTwoPhaseEvent,这个函数的作用分别为事件注册捕获和冒泡事件

function registerTwoPhaseEvent(registrationName, dependencies) {
  // 冒泡 onClick
  registerDirectEvent(registrationName, dependencies);
  // 捕获 onCaptureClick
  registerDirectEvent(registrationName + "Capture", dependencies);
}

registerDirectEvent 函数内部做了两件事:

  • registrationNameDependencies 建立映射关系
  • 将所有的原生事件保存到 allNativeEvents 变量中
// registrationNameDependencies 保存 react 事件和其依赖的事件的映射
// {onClick: ["click"], onCapture: ["click"], onMouseEnter: ["mouseout", "mouseover"] ... }
const registrationNameDependencies = {};

// allNativeEvents 集合中就会有 click、cancel 等 81 种事件,["click", "cancel"...]
const allNativeEvents = new Set();

function registerDirectEvent(registrationName, dependencies) {
  registrationNameDependencies[registrationName] = dependencies;

  for (let i = 0; i < dependencies.length; i++) {
    allNativeEvents.add(dependencies[i]);
  }
}

listenToNativeEvent

这个函数做了一次转发,在调用 addTrappedEventListener 之前,算出捕获还是冒泡的 flag

ps: 不知道为什么 react 有很多这种类型的函数,这种写法复用性很强吗?

/**
 * @param domEventName - 事件名,如 click
 * @param isCapturePhaseListener - 是否是捕获阶段,false 代表冒泡,true 代表捕获
 * @param target - <div id="root"></div>,如果是 selectionChange 事件,这个值是 document
 */
function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
  // 冒泡是 0,捕获是 4
  let eventSystemFlags = 0;
  if (isCapturePhaseListener) {
    eventSystemFlags |= IS_CAPTURE_PHASE;
  }
  /**
   * @param targetContainer - <div id="root"></div>,如果是 selectionChange 事件,这个值是 document
   * @param domEventName - 事件名,如 click
   * @param eventSystemFlags - 事件类型标志,冒泡是 0,捕获是 4
   * @param isCapturePhaseListener - 是否是捕获阶段,false 代表冒泡,true 代表捕获
   */
  addTrappedEventListener(
    target,
    domEventName,
    eventSystemFlags,
    isCapturePhaseListener
  );
}

addTrappedEventListener

这函数有一部分不会运行代码,我这里直接忽略了

它主要做了 3 件事:

  1. createEventListenerWrapperWithPriority 构造事件监听器
  2. 判断浏览器是否支持 passive,如果支持将 touchstart/touchmove/wheel 需要将 passive 设置为 true
  3. 根据 isCapturePhaseListener 的值挂载的事件是冒泡还是捕获

react 挂载的事件永远不会卸载,并且一个事件只会挂在一次

/**
 * @param targetContainer - <div id="root"></div>,如果是 selectionChange 事件,这个值是 document
 * @param domEventName - 事件名,如 click
 * @param eventSystemFlags - 事件类型标志,冒泡是 0,捕获是 4
 * @param isCapturePhaseListener - 是否是捕获阶段
 */
function addTrappedEventListener(
  targetContainer,
  domEventName,
  eventSystemFlags,
  isCapturePhaseListener
) {
  /**
   * 构造事件监听器
   * @return 事件监听函数,这里的 listener 函数接收一个事件参数 e,内部叫做 nativeEvent,就是 x.addEventListener("click", e => {}) 中的 e
   * @param targetContainer - <div id="root"></div>,如果是 selectionChange 事件,这个值是 document
   * @param domEventName - 事件名,如 click
   * @param eventSystemFlags - 事件类型标志,冒泡是 0,捕获是 4
   */
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags
  );

  // touchstart touchmove wheel 需要将 passive 设置为 true
  let isPassiveListener = undefined;
  if (passiveBrowserEventsSupported) {
    if (
      domEventName === "touchstart" ||
      domEventName === "touchmove" ||
      domEventName === "wheel"
    ) {
      isPassiveListener = true;
    }
  }

  let unsubscribeListener;

  /**
   * 根据是否是捕获阶段,挂载不同的事件
   * @param targetContainer - <div id="root"></div>,如果是 selectionChange 事件,这个值是 document
   * @param domEventName - 事件名,如 click
   * @param listener
   */
  if (isCapturePhaseListener) {
    if (isPassiveListener !== undefined) {
      // 捕获事件,passive 为 true
      unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
        targetContainer,
        domEventName,
        listener,
        isPassiveListener
      );
    } else {
      // 捕获事件,passive 为 false
      unsubscribeListener = addEventCaptureListener(
        targetContainer,
        domEventName,
        listener
      );
    }
  } else {
    if (isPassiveListener !== undefined) {
      // 冒泡事件,passive 为 true
      unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
        targetContainer,
        domEventName,
        listener,
        isPassiveListener
      );
    } else {
      // 冒泡事件,passive 为 false
      unsubscribeListener = addEventBubbleListener(
        targetContainer,
        domEventName,
        listener
      );
    }
  }
}

createEventListenerWrapperWithPriority

react 将事件按照优先级分为:

  • 离散事件优先级,例如:click/input 等触发的更新任务,优先级为 2(0b0000000000000000000000000000010)
  • 连续事件优先级,例如:wheel/mouseout/mouseover 等,连续触发的事件,优先级为 8(0b0000000000000000000000000001000)
  • 默认事件优先级,例如:setTimeout 触发的更新任务,优先级为 32(0b0000000000000000000000000100000)
  • 闲置事件优先级,优先级最低,优先级为 536870912(0b0100000000000000000000000000000)

通过 getEventPriority 函数获取事件优先级(message 事件单独处理):

  • 如果是离散型,调用 dispatchDiscreteEvent函数
  • 如果是连续型,调用 dispatchContinuousEvent 函数,
  • 都不是,则调用 dispatchEvent 函数

dispatchDiscreteEventdispatchContinuousEvent 的作用是设置当前的事件优先级,然后再调用 dispatchEvent 完成事件函数处理

/**
 * 根据优先级构造事件监听器
 * @return 事件监听函数,这里的 listener 函数接收一个事件参数 e,内部叫做 nativeEvent,就是 x.addEventListener("click", e => {}) 中的 e
 * @param targetContainer - <div id="root"></div>,如果是 selectionChange 事件,这个值是 document
 * @param domEventName - 事件名,如 click
 * @param eventSystemFlags - 事件类型标志,冒泡是 0,捕获是 4
 */
function createEventListenerWrapperWithPriority(
  targetContainer,
  domEventName,
  eventSystemFlags
) {
  // 获取事件优先级:离散型最高,连续型其次,接着默认事件
  const eventPriority = getEventPriority(domEventName);
  let listenerWrapper;
  switch (eventPriority) {
    // 离散事件
    case DiscreteEventPriority: // 0b0000000000000000000000000000010
      listenerWrapper = dispatchDiscreteEvent;
      break;
    // 连续事件
    case ContinuousEventPriority: // 0b0000000000000000000000000001000
      listenerWrapper = dispatchContinuousEvent;
      break;
    // 默认事件
    case DefaultEventPriority: // 0b0000000000000000000000000100000
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  /**
   * @param this - null
   * @param domEventName - 事件名,如 click
   * @param eventSystemFlags - 事件类型标志,冒泡是 0,捕获是 4
   * @param targetContainer - <div id="root"></div>,如果是 selectionChange 事件,这个值是 document
   * @description dispatchEvent 函数接收四个参数,其中三个参数:domEventName、eventSystemFlags、targetContainer 这里已经绑定
   * @description 还有一个 nativeEvent 参数是 DOM 事件参数,xx.addEventListener("click", e => {}) 这里的 e 就是 nativeEvent,由外部传进来
   */
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer
  );
}

dispatchEvent

通过判断 enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay 是为 true 分别调用 dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplaydispatchEventOriginal

但是 enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay 变量是写死的,一直为 true

ps: react 中有很多这样的写法,我也不知道为什么要这样写

/**
 * @param domEventName - 事件名称
 * @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
 * @param targetContainer -  <div id="root"><div>
 * @param nativeEvent - 浏览器的 event
 */
function dispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent
): void {
  if (!_enabled) {
    return;
  }
  if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
    /**
     * @param domEventName - 事件名称
     * @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
     * @param targetContainer -  <div id="root"><div>
     * @param nativeEvent - 浏览器的 event
     */
    dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
      domEventName,
      eventSystemFlags,
      targetContainer,
      nativeEvent
    );
  } else {
    dispatchEventOriginal(
      domEventName,
      eventSystemFlags,
      targetContainer,
      nativeEvent
    );
  }
}

dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay

这个函数主要调用了 2个函数:

  1. findInstanceBlockingEvent
  2. dispatchEventForPluginEventSystem

findInstanceBlockingEvent 主要作用是查找当前事件是否被阻塞,从当前节点递归向上遍历节点,一直到根节点,找到阻塞事件(这个组件是否有事件在执行),如果有返回这个节点的 fiber,否者返回 null

dispatchEventForPluginEventSystem 找到祖先实例,批量调用 dispatchEventsForPlugins(如果未开启批量,则开启批量调用)

/**
 * @param domEventName - 事件名称
 * @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
 * @param targetContainer - <div id="root"><div>
 * @param nativeEvent - 浏览器的 event
 */
function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent
) {
  // 从 nativeEvent 中获取到 e.target,然后找到对应的 fiber,将 fiber 赋值给 return_targetInst
  // 查找当前事件处理函数是否被阻塞了
  // 从当前组件开始,递归向上遍历组件树,找到阻塞的事件(这个组件是否有事件在执行),如果有返回这个 fiber,如果没有返回 null
  // createRoot 时,blockedOn 为 null
  let blockedOn = findInstanceBlockingEvent(
    domEventName,
    eventSystemFlags,
    targetContainer,
    nativeEvent
  );
  if (blockedOn === null) {
    /**
     * @param domEventName - 事件名称
     * @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
     * @param nativeEvent - 浏览器的 event
     * @param targetInst - return_targetInst,nativeEvent 中的 e.target 所对应的 fiber
     * @param targetContainer - <div id="root"><div>
     */
    // 将原生事件转化为合成事件,并将合成事件传递给后续的事件处理

    dispatchEventForPluginEventSystem(
      domEventName,
      eventSystemFlags,
      nativeEvent,
      return_targetInst,
      targetContainer
    );
    // 将连续触发类型的事件重置为无事件
    clearIfContinuousEvent(domEventName, nativeEvent);
    return;
  }
}

findInstanceBlockingEvent

  1. 通过调用 getEventTarget 函数找到目标节点(e.target)
  2. 通过调用 getClosestInstanceFromNode 函数找到目标节点对应的(fiber)
  3. 通过调用 getNearestMountedFiber 函数找到目标节点最近挂载点

再正常情况下页面都只有一个事件在执行,这个函数返回返回的都是 null

// 查找当前事件是否被阻塞,从当前节点递归向上遍历节点,一直到根节点,找到阻塞事件(这个组件是否有事件在执行),如果有返回这个节点的 fiber,否者返回 null
export function findInstanceBlockingEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent
): null | Container | SuspenseInstance {
  // TODO: Warn if _enabled is false.

  return_targetInst = null;

  // 获取原生事件的 target,当前节点(e.target)这种
  // 在 vite 创建的项目中,react logo 会有动画,这里的 nativeEvent 是 animationstart 事件
  // 同时 img 还有个 load 事件
  const nativeEventTarget = getEventTarget(nativeEvent);
  // 获取当前节点(e.target)对应的 fiber
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);

  if (targetInst !== null) {
    /**
     * @param {Fiber} targetInst - 当前节点(e.target)对应的 fiber
     * @description 在 createRoot 时 nearestMounted 为 targetInst
     */
    const nearestMounted = getNearestMountedFiber(targetInst);
    if (nearestMounted === null) {
      // This tree has been unmounted already. Dispatch without a target.
      targetInst = null;
    } else {
      const tag = nearestMounted.tag;
      // 在 createRoot 时,下面的代码不会执行
      if (tag === SuspenseComponent) {
        const instance = getSuspenseInstanceFromFiber(nearestMounted);
        if (instance !== null) {
          // Queue the event to be replayed later. Abort dispatching since we
          // don't want this event dispatched twice through the event system.
          // TODO: If this is the first discrete event in the queue. Schedule an increased
          // priority for this boundary.
          return instance;
        }
        // This shouldn't happen, something went wrong but to avoid blocking
        // the whole system, dispatch the event without a target.
        // TODO: Warn.
        targetInst = null;
      } else if (tag === HostRoot) {
        const root: FiberRoot = nearestMounted.stateNode;
        if (isRootDehydrated(root)) {
          // If this happens during a replay something went wrong and it might block
          // the whole system.
          return getContainerFromFiber(nearestMounted);
        }
        targetInst = null;
      } else if (nearestMounted !== targetInst) {
        // If we get an event (ex: img onload) before committing that
        // component's mount, ignore it for now (that is, treat it as if it was an
        // event on a non-React tree). We might also consider queueing events and
        // dispatching them after the mount.
        targetInst = null;
      }
    }
  }
  return_targetInst = targetInst;
  // We're not blocked on anything.
  return null;
}

dispatchEventForPluginEventSystem

/**
 * 调度事件处理函数的核心函数
 * @param domEventName - 事件名称
 * @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
 * @param nativeEvent - 浏览器的 event
 * @param targetInst - return_targetInst,nativeEvent 中的 e.target 所对应的 fiber
 * @param targetContainer -  <div id="root"><div>
 */
// 找到祖先实例,批量调用 dispatchEventsForPlugins(如果未开启批量,则开启批量调用)
export function dispatchEventForPluginEventSystem(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget
): void {
  let ancestorInst = targetInst;
  /**
   * @param domEventName - 事件名称
   * @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
   * @param nativeEvent - 浏览器的 event
   * @param targetInst - ancestorInst,nativeEvent 中的 e.target 所对应的 fiber
   * @param targetContainer - createRoot 时 <div id="root"><div>
   */
  // 找到祖先实例后执行 dispatchEventsForPlugins(如果未开启批量,则开启批量调用)
  batchedUpdates(() =>
    dispatchEventsForPlugins(
      domEventName,
      eventSystemFlags,
      nativeEvent,
      ancestorInst,
      targetContainer
    )
  );
}

dispatchEventsForPlugins

这个函数内部调用了 react 内部两个核心函数

  1. extractEvents
  2. processDispatchQueue

extractEvents 作用:从原生事件中提取合成事件,并放入 dispatchQueue 队列中

processDispatchQueue 作用:从 dispatchQueue 中取出合成事件,并根据事件类型和目标元素,找到对应的监听器并执行

dispatchQueue 是一个事件队列,包含两个属性:eventlisteners

  • event 是合成对象
  • listeners 是数组,里面包含了监听函数

dispatchEventsForPlugins 执行前,dispatchQueue 是个空数组,执行完之后,dispatchQueue 可能就包含事件了

/**
 * @param domEventName - 事件名称
 * @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
 * @param nativeEvent - 浏览器的 event
 * @param targetInst - ancestorInst,nativeEvent 中的 e.target 所对应的 fiber
 * @param targetContainer - createRoot 时 <div id="root"><div>
 */
function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget
): void {
  // 获取原生事件的 target,e.target 这种
  const nativeEventTarget = getEventTarget(nativeEvent);
  // 事件类型是:DispatchEntry
  // 包含两个属性:event 和 listeners
  //  - event 是一个 SyntheticEvent 对象
  //    - isPersistent: () => boolean(是否是连续的)
  //    - isPropagationStopped: () => boolean
  //    - _dispatchInstances?: null | Array<Fiber | null> | Fiber
  //    - _dispatchListeners?: null | Array<Function> | Function
  //    - _targetInst: Fiber
  //    - nativeEvent: Event(原生事件)
  //    - target?: mixed
  //    - relatedTarget?: mixed
  //    - type: string
  //    - currentTarget: null | EventTarget
  //  - listeners 是一个数组
  //    - instance: null | Fiber
  //    - listener: Function(事件函数,比如 onClick)
  //    - currentTarget: EventTarget
  const dispatchQueue: DispatchQueue = [];

  /**
   * @param dispatchQueue - []
   * @param domEventName - 事件名称
   * @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
   * @param nativeEvent - 浏览器的 event
   * @param nativeEventTarget - 原生事件对应的 target
   * @param targetInst - ancestorInst,nativeEvent 中的 e.target 所对应的 fiber
   * @param targetContainer - createRoot 时 <div id="root"><div>
   */
  // 从原生事件中提取合成事件,并放入 dispatchQueue 队列中
  //  - 遍历 target(事件目标节点) 到 targetContainer(根节点) 之间的所有节点
  //  - 检查它们是否有注册过相应类型的事件监听器
  //  - 如果有,创建一个 SyntheticEvent 对象,包含了 type、target、currentTarget、eventPhase、timestamp 等。
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer
  );
  // 从 dispatchQueue 中取出合成事件,并根据事件类型和目标元素,找到对应的监听器并执行
  //  - 遍历 dispatchQueue 中的每个 SyntheticEvent 对象
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

extractEvents

由于 react 把事件分为 5 类,所以从原生事件中提取合成事件也也会分成五种

/**
 * @param dispatchQueue - []
 * @param domEventName - 事件名称
 * @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
 * @param nativeEvent - 浏览器的 event
 * @param nativeEventTarget - 原生事件对应的 target
 * @param targetInst - ancestorInst,nativeEvent 中的 e.target 所对应的 fiber
 * @param targetContainer - createRoot 时 <div id="root"><div>
 */
function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget
) {
  SimpleEventPlugin.extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer
  );
  const shouldProcessPolyfillPlugins =
    (eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
  // 冒泡阶段执行,捕获阶段不执行
  if (shouldProcessPolyfillPlugins) {
    EnterLeaveEventPlugin.extractEvents(
      dispatchQueue,
      domEventName,
      targetInst,
      nativeEvent,
      nativeEventTarget,
      eventSystemFlags,
      targetContainer
    );
    ChangeEventPlugin.extractEvents(
      dispatchQueue,
      domEventName,
      targetInst,
      nativeEvent,
      nativeEventTarget,
      eventSystemFlags,
      targetContainer
    );
    SelectEventPlugin.extractEvents(
      dispatchQueue,
      domEventName,
      targetInst,
      nativeEvent,
      nativeEventTarget,
      eventSystemFlags,
      targetContainer
    );
    BeforeInputEventPlugin.extractEvents(
      dispatchQueue,
      domEventName,
      targetInst,
      nativeEvent,
      nativeEventTarget,
      eventSystemFlags,
      targetContainer
    );
  }
}

SimpleEventPlugin.extractEvents

从原生事件中提取合成事件,并放入 dispatchQueue 队列中

  • 遍历 target(事件目标节点) 到 targetContainer(根节点) 之间的所有节点
  • 检查它们是否有注册过相应类型的事件监听器
    • 如果有,创建一个 SyntheticEvent 对象,包含了 typetargetcurrentTargeteventPhasetimestamp
  1. 调用 topLevelEventsToReactNames,获取 react 事件名
  2. 根据事件名绑定合成事件对象
    • SyntheticKeyboardEvent
    • SyntheticFocusEvent
    • SyntheticMouseEvent
    • SyntheticDragEvent
    • SyntheticTouchEvent
    • SyntheticAnimationEvent
    • SyntheticTransitionEvent
    • SyntheticUIEvent
    • SyntheticWheelEvent
    • SyntheticClipboardEvent
    • SyntheticPointerEvent
  3. 收集合成事件的监听器:遍历目标节点到祖先节点,找到所有注册了该事件类型的监听器,将它们存储在 dispatchQueue
    • dispatchQueue 类型是 DispatchListener[],它有这些属性:
      • instance: null | Fiber
      • listener: Function,例如:<div onClick={xxx} /> xxx 函数
      • currentTarget: EventTarget,例如:button
/**
 * @param dispatchQueue - []
 * @param domEventName - 事件名称
 * @param eventSystemFlags - 事件类型标志,捕获阶段是 4,冒泡阶段是 0
 * @param nativeEvent - 浏览器的 event
 * @param nativeEventTarget - 原生事件对应的 target
 * @param targetInst - ancestorInst,nativeEvent 中的 e.target 所对应的 fiber
 * @param targetContainer - createRoot 时 <div id="root"><div>
 */
function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget
): void {
  // click => onClick
  // domEventName: click
  // reactName: onClick
  const reactName = topLevelEventsToReactNames.get(domEventName);
  if (reactName === undefined) {
    return;
  }
  let SyntheticEventCtor = SyntheticEvent;
  let reactEventType: string = domEventName;
  // 根据事件名绑定相应的合成事件:
  //  - SyntheticKeyboardEvent
  //  - SyntheticFocusEvent
  //  - SyntheticMouseEvent
  //  - SyntheticDragEvent
  //  - SyntheticTouchEvent
  //  - SyntheticAnimationEvent
  //  - SyntheticTransitionEvent
  //  - SyntheticUIEvent
  //  - SyntheticWheelEvent
  //  - SyntheticClipboardEvent
  //  - SyntheticPointerEvent
  switch (domEventName) {
    case "keypress":
      // Firefox creates a keypress event for function keys too. This removes
      // the unwanted keypress events. Enter is however both printable and
      // non-printable. One would expect Tab to be as well (but it isn't).
      if (getEventCharCode(((nativeEvent: any): KeyboardEvent)) === 0) {
        return;
      }
    /* falls through */
    case "keydown":
    case "keyup":
      SyntheticEventCtor = SyntheticKeyboardEvent;
      break;
    case "focusin":
      reactEventType = "focus";
      SyntheticEventCtor = SyntheticFocusEvent;
      break;
    case "focusout":
      reactEventType = "blur";
      SyntheticEventCtor = SyntheticFocusEvent;
      break;
    case "beforeblur":
    case "afterblur":
      SyntheticEventCtor = SyntheticFocusEvent;
      break;
    case "click":
      // Firefox creates a click event on right mouse clicks. This removes the
      // unwanted click events.
      if (nativeEvent.button === 2) {
        return;
      }
    /* falls through */
    case "auxclick":
    case "dblclick":
    case "mousedown":
    case "mousemove":
    case "mouseup":
    // TODO: Disabled elements should not respond to mouse events
    /* falls through */
    case "mouseout":
    case "mouseover":
    case "contextmenu":
      SyntheticEventCtor = SyntheticMouseEvent;
      break;
    case "drag":
    case "dragend":
    case "dragenter":
    case "dragexit":
    case "dragleave":
    case "dragover":
    case "dragstart":
    case "drop":
      SyntheticEventCtor = SyntheticDragEvent;
      break;
    case "touchcancel":
    case "touchend":
    case "touchmove":
    case "touchstart":
      SyntheticEventCtor = SyntheticTouchEvent;
      break;
    case ANIMATION_END:
    case ANIMATION_ITERATION:
    case ANIMATION_START:
      SyntheticEventCtor = SyntheticAnimationEvent;
      break;
    case TRANSITION_END:
      SyntheticEventCtor = SyntheticTransitionEvent;
      break;
    case "scroll":
      SyntheticEventCtor = SyntheticUIEvent;
      break;
    case "wheel":
      SyntheticEventCtor = SyntheticWheelEvent;
      break;
    case "copy":
    case "cut":
    case "paste":
      SyntheticEventCtor = SyntheticClipboardEvent;
      break;
    case "gotpointercapture":
    case "lostpointercapture":
    case "pointercancel":
    case "pointerdown":
    case "pointermove":
    case "pointerout":
    case "pointerover":
    case "pointerup":
      SyntheticEventCtor = SyntheticPointerEvent;
      break;
    default:
      // Unknown event. This is used by createEventHandle.
      break;
  }

  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;

  const accumulateTargetOnly =
    !inCapturePhase &&
    // TODO: ideally, we'd eventually add all events from
    // nonDelegatedEvents list in DOMPluginEventSystem.
    // Then we can remove this special list.
    // This is a breaking change that can wait until React 18.
    domEventName === "scroll";

  /**
   * @param targetFiber - targetInst,ancestorInst,nativeEvent 中的 e.target 所对应的 fiber
   * @param reactName - react 事件名,例如 onClick
   * @param nativeEventType - nativeEvent.type
   * @param inCapturePhase - 例如:load 在捕获和冒泡阶段都会触发
   * @param accumulateTargetOnly - 是不是 scroll
   * @param nativeEvent - 浏览器的 event
   */
  // 收集合成事件的监听器
  // 遍历目标节点到祖先节点,找到所有注册了该事件类型的监听器,将它们存储在一个数组中
  // 数组类型是 DispatchListener[],它有这些属性:
  //  - instance: null | Fiber
  //  - listener: Function 例如:<div onClick={xxx} /> xxx 函数
  //  - currentTarget: EventTarget,例如如 button
  const listeners = accumulateSinglePhaseListeners(
    targetInst,
    reactName,
    nativeEvent.type,
    inCapturePhase,
    accumulateTargetOnly,
    nativeEvent
  );
  if (listeners.length > 0) {
    // Intentionally create event lazily.
    // 创建一个合成事件 SyntheticEvent,放到队列中
    const event: ReactSyntheticEvent = new SyntheticEventCtor(
      reactName,
      reactEventType,
      null,
      nativeEvent,
      nativeEventTarget
    );
    dispatchQueue.push({ event, listeners });
  }
}

accumulateSinglePhaseListeners

遍历目标节点到祖先节点,找到所有注册了该事件类型的监听器,存储在 DispatchListener[]

/**
 * @param targetFiber - nativeEvent 中的 e.target 所对应的 fiber
 * @param reactName - react 事件名,例如 onClick
 * @param nativeEventType - nativeEvent.type
 * @param inCapturePhase - 例如:load 在捕获和冒泡阶段都会触发
 * @param accumulateTargetOnly - 是不是 scroll
 * @param nativeEvent - 浏览器的 event
 */
// 收集合成事件的监听器
// 遍历目标节点到祖先节点,找到所有注册了该事件类型的监听器,将它们存储在一个数组中
// 数组类型是 DispatchListener[],它有这些属性:
//  - instance: null | Fiber
//  - listener: Function
//  - currentTarget: EventTarget
export function accumulateSinglePhaseListeners(
  targetFiber: Fiber | null,
  reactName: string | null,
  nativeEventType: string,
  inCapturePhase: boolean,
  accumulateTargetOnly: boolean,
  nativeEvent: AnyNativeEvent
): Array<DispatchListener> {
  // 捕获阶段的事件名:onClickCapture
  // 冒泡阶段的事件名:onClick
  const captureName = reactName !== null ? reactName + "Capture" : null;
  const reactEventName = inCapturePhase ? captureName : reactName;
  let listeners: Array<DispatchListener> = [];

  // 目标属性,
  let instance = targetFiber;
  let lastHostComponent = null;

  while (instance !== null) {
    // 用 vite 创建的项目
    // stateNode: img,            tag: 5
    // stateNode: a ,             tag: 5
    // stateNode: div,            tag: 5
    // stateNode: div.app,        tag: 5
    // stateNode: null,           tag: 5
    // stateNode: null,           tag: 8
    // stateNode: FiberRootNode,  tag: 3
    // tag 类型是 WorkTag,文件:packages/react-reconciler/src/ReactWorkTags.js
    // stateNode 与 fiber 相关的 dom
    const { stateNode, tag } = instance;
    // Handle listeners that are on HostComponents (i.e. <div>)
    if (
      (tag === HostComponent ||
        (enableFloat ? tag === HostHoistable : false) ||
        (enableHostSingletons ? tag === HostSingleton : false)) &&
      stateNode !== null
    ) {
      lastHostComponent = stateNode;

      // Standard React on* listeners, i.e. onClick or onClickCapture
      if (reactEventName !== null) {
        /**
         * @param inst - targetFiber,nativeEvent 中的 e.target 所对应的 fiber
         * @param registrationName - reactEventName: isCapture ? onClickCapture : onClick
         */
        const listener = getListener(instance, reactEventName);
        if (listener != null) {
          listeners.push(
            createDispatchListener(instance, listener, lastHostComponent)
          );
        }
      }
    }
    if (accumulateTargetOnly) {
      break;
    }
    instance = instance.return;
  }
  return listeners;
}

getListener

/**
 * @param  inst - targetFiber,nativeEvent 中的 e.target 所对应的 fiber
 * @param registrationName - isCapture ? onClickCapture : onClick
 */
export default function getListener(
  inst: Fiber,
  registrationName: string
): Function | null {
  const stateNode = inst.stateNode;
  if (stateNode === null) {
    // Work in progress (ex: onload events in incremental mode).
    return null;
  }
  const props = getFiberCurrentPropsFromNode(stateNode);
  if (props === null) {
    // Work in progress.
    return null;
  }
  // 从 props 中获取到事件
  // <div onClick={xxx} />
  const listener = props[registrationName];
  // 如果 inst.type 是 button,input, select, textarea
  // 并且 props.disabled 为 true
  // 则 onClick, ... 不会有监听器
  if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
    return null;
  }

  if (listener && typeof listener !== "function") {
    throw new Error(
      `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`
    );
  }

  return listener;
}

processDispatchQueue

dispatchQueue 中取出合成事件,并根据事件类型和目标元素,找到对应的监听器并执行

  • 遍历 dispatchQueue 中的每个 SyntheticEvent 对象
export function processDispatchQueue(
  dispatchQueue: DispatchQueue,
  eventSystemFlags: EventSystemFlags
): void {
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  for (let i = 0; i < dispatchQueue.length; i++) {
    const { event, listeners } = dispatchQueue[i];
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
  }
  rethrowCaughtError();
}

processDispatchQueueItemsInOrder

// 捕获阶段是从 root 向下传播一直到 target
//  - [rootListener, ..., targetListener]
//  - 模拟捕获,就需要从最后一个节点开始执行回调函数
// 冒泡阶段是从 target 向上传播传播一直到 root
//  - [targetListener, ..., rootListener]
//  - 模拟冒泡,就需要从第一个节点开始执行回调函数
// 在每次执行回调函数之前,检查当前是否处于批量更新模式(batchedUpdates)
//  - 如果不是,则开启批量更新模式,并在所有回调函数执行完毕后再关闭批量更新模式。这样可以避免多次渲染组件,提高性能
/**
 *
 * @param event - 合成事件
 * @param dispatchListeners - DispatchListener[]
 *  DispatchListener:
 *    - instance: null | Fiber,对应的 fiber
 *    - listener: Function,事件函数
 *    - currentTarget: EventTarget,例如 button
 * @param inCapturePhase - 是否是捕获阶段
 */
function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean
): void {
  let previousInstance;
  if (inCapturePhase) {
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const { instance, currentTarget, listener } = dispatchListeners[i];
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      /**
       * @param event - 例如:button 对应的 fiber
       * @param listener - 例如:事件函数
       * @param currentTarget - 例如:button
       */
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  } else {
    for (let i = 0; i < dispatchListeners.length; i++) {
      const { instance, currentTarget, listener } = dispatchListeners[i];
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  }
}

executeDispatch

function executeDispatch(
  event: ReactSyntheticEvent,
  listener: Function,
  currentTarget: EventTarget
): void {
  const type = event.type || "unknown-event";
  event.currentTarget = currentTarget;
  // 用于在执行回调函数时捕获第一个错误并记录它
  // 这个方法是在 React 的错误处理机制中使用的,它确保了在 React 组件中的回调函数中发生的错误不会导致整个应用程序崩溃
  // 如果在回调函数中发生错误,React 会记录错误并继续执行应用程序,而不是崩溃
  invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
  event.currentTarget = null;
}

最后

reactcreateRoot 阶段所作的事情全部结束,接下来将进入 render 阶段

剖析 React 任务调度机制:scheduleCallback 实现原理

unstable_scheduleCallback 函数是 react 任务调度核心函数,主要作用是根据任务的优先级进行任务调度

它接收一个优先级(priorityLevel)、一个回调函数(callback)和一个延迟时间({delay: number})作为参数,返回一个任务对象

这个函数主要做了三件事:

  1. 设置任务开始时间
  2. 设置任务过期时间
    • 构建任务队列
  3. 根据任务优先级,调度任务
    • 延时任务和非延时任务调度

这个函数的核心内容是任务队列构建任务调度,它们分别在【构建任务队列】和【延时任务调度和非延时任务调度】中介绍

设置任务开始时间

通过 getCurrentTime() 获取当前时间,然后加上延迟时间(如果有),得到任务开始时间

延迟时间是从外面传递进来的,可能与 startTransition 有关,它可以在不阻塞主线程的情况下执行一些低优先级的更新

源码:

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

设置任务过期时间

任务过期时间:startTime + timeout

timeout 的值取决于任务优先级(priorityLevel),它们的对应关系如下:

  • ImmediatePriority-1 (立即执行)
  • UserBlockingPriority250 (250ms 后执行)
  • NormalPriority5000 (5s 后执行)
  • LowPriority10000 (10s 后执行)
  • IdlePriority1073741823 (1073741.823s 后执行)

react 这么设计是因为 expirationTime 的值越小,表示任务越紧急,就需要优先执行

这里有一点需要注意:

  • IdlePriority 这个优先级的过期时间大概在 12 天,这意味着几乎不会过期,通常用于一些不紧急的后台任务,比如数据预加载、用户不可见的更新等
  • 这个优先级的任务会被延迟到其他更重要的任务都完成后再执行

源码:

var timeout;
switch (priorityLevel) {
  case ImmediatePriority:
    timeout = IMMEDIATE_PRIORITY_TIMEOUT;
    break;
  case UserBlockingPriority:
    timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
    break;
  case IdlePriority:
    timeout = IDLE_PRIORITY_TIMEOUT;
    break;
  case LowPriority:
    timeout = LOW_PRIORITY_TIMEOUT;
    break;
  case NormalPriority:
  default:
    timeout = NORMAL_PRIORITY_TIMEOUT;
    break;
}

var expirationTime = startTime + timeout;

根据任务优先级,调度任务

新建任务

task 是一个任务,有以下几个属性:

type Task = {
  id: number;
  callback: Callback | null;
  priorityLevel: PriorityLevel;
  startTime: number;
  expirationTime: number;
  sortIndex: number;
  isQueued?: boolean;
};

这些属性的具体作用:

  • id: 使用的是一个全局变量 taskIdCounter,每次创建任务的时候都会自动加一,用于区分不同的任务
  • callback:一个函数,表示要执行的任务,它会在调度器安排好时间后被调用
    • 这个取决于你在里面做什么,比如更新组件的状态、渲染组件、或者执行其他逻辑
  • priorityLevel:优先级,会影响任务何时被执行,有五种:ImmediatePriorityUserBlockingPriorityIdlePriorityLowPriorityNormalPriority
  • startTime:任务开始时间,它是使用 getCurrentTime 函数获取的微秒级别的当前时间
  • expirationTime:任务过期的时间,它是根据 startTimepriorityLevel 计算出来的。过期时间越小,表示任务越紧急,越需要优先执行
  • sortIndex:任务在队列中的排序索引,默认为 -1。它实际上会被设置成 startTimeexpirationTime,以便在插入队列时按照升序排列
    • 保证过期时间越小,越紧急的任务排在最前面
  • isQueued:任务是否已经被加入到队列中。这个属性是为了避免重复插入或删除任务而设置的
    • 在性能分析模式下,会加入这个属性,目前这个值没有启用

如何构建任务队列(见【构建任务队列】)

任务调度

根据任务开始时间(startTime)和当前时间(currentTime)来判断是否是延迟任务

  • 如果是,就把任务加入到 timerQueue 中,并根据任务的开始时间,安排一个定时器(requestHostTimeout)来执行任务;
  • 如果不是,就把任务加入到 taskQueue 中,并根据任务的过期时间,安排一个回调函数(requestHostCallback)来执行任务

timerQueue:用来存放延迟任务,也就是那些还没有到达执行时间的任务。它们会在定时器触发后被执行

taskQueue:用来存放非延迟任务,也就是那些可以立即执行的任务。它们会在浏览器空闲时被执行

如果任务的 startTime 大于 currentTime,说明任务还没有到达执行的时间,需要等待一段时间(因为立即执行的任务开始 timeout-1)

执行过程:

  1. 对于延迟任务,排序索引是开始时间(升序排序,索引:newTask.sortIndex = startTime)

    1. 把延迟任务放到 timerQueue 队列中(push(timerQueue, newTask)
    2. 判定当前是否有其他的非延迟任务(peek(taskQueue) === null)或者更早的延迟任务(newTask === peek(timeQueue))
      • peek(taskQueue) === null 说明非延迟任务列表没有任务
      • newTask === peek(timeQueue) 说明刚加入的任务就是最紧急的任务
      • 是否已经安排了一个定时
        • 是:取消定时器
        • 不是:标记已经安排了一个定时器
        • 为什么要这么做:说明当前的任务比之前的任务紧急
      • requestHostTimeout(handleTimeout, startTime - currentTime) 是设置一个定时器,在 startTime - currentTime 这么长的时间后执行 handleTimeout 这个函数,它会从 timerQueue 中取出最紧急的延迟任务并执行它。
  2. 对于非延迟任务,排序索引是过期时间(升序排列,排序索引:newTask.sortIndex = expirationTime)

    1. 把非延迟任务放到 taskQueue 队列中(push(taskQueue, newTask)
    2. 判断当前是否已经安排了一个回调函数(!isHostCallbackScheduled)或者正在执行工作(!isPerformingWork)
      • requestHostCallback(flushWork) 安排一个回调函数,在浏览器空闲时执行 flushWork 这个函数,它会从 taskQueue 中取出最紧急的非延迟任务并执行它。

任务调度的具体过程(见【延时任务调度和非延时任务调度】)

源码简化:

if (startTime > currentTime) {
  newTask.sortIndex = startTime;
  push(timerQueue, newTask);
  if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
    if (isHostTimeoutScheduled) {
      cancelHostTimeout();
    } else {
      isHostTimeoutScheduled = true;
    }
    requestHostTimeout(handleTimeout, startTime - currentTime);
  }
} else {
  newTask.sortIndex = expirationTime;
  push(taskQueue, newTask);
  if (!isHostCallbackScheduled && !isPerformingWork) {
    isHostCallbackScheduled = true;
    requestHostCallback(flushWork);
  }
}

构建任务队列

构建任务队列时,根据任务的 sortIndex 优先级,安排任务的执行顺序

  • timerQueuesortIndexstartTime
  • taskQueuesortIndexexpirationTime

timerQueuetaskQueue 队列是个最小堆,执行时每次取出堆顶任务

如何保证元素添加到堆中后,能够快速放到合适的位置呢?

构建最小堆

一个元素的父节点的索引,就是那个元素的索引除以 2,向下取整

由于这里使用数组存放二叉堆,所以元素索引的方式:(index - 1) >>> 1

  • 无符号右移,返回值一定是一个非负数
  • index - 1 的作用是对应数组的索引,因为数组的索引是从 0 开始的

遍历条件通过判断父元素索引是否大于 0 来实现最少遍历次数

比如说当前的索引是 12,要在 12 后面插入一个新的元素:

  • 11 -> 5
  • 5 -> 2
  • 2 -> 1

通过三次遍历就能找到将一个元素放合适位置了

function push<T: Node>(heap: Heap<T>, node: T): void {
  const index = heap.length;
  heap.push(node);
  siftUp(heap, node, index);
}
function siftUp<T: Node>(heap: Heap<T>, node: T, i: number): void {
  let index = i;
  while (index > 0) {
    const parentIndex = (index - 1) >>> 1;
    const parent = heap[parentIndex];
    if (compare(parent, node) > 0) {
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      return;
    }
  }
}

从堆顶取出元素

react 从堆中取去一个元素有两种方法:

  • peek
  • pop

这两个函数的区别是:

  • peek 方法只是取出堆顶的元素,不会删除堆顶的元素
  • pop 方法会删除堆顶的元素,并且重新安排堆的结构(最小堆)

这里主要看 pop 是如何实现的

删除堆顶元素后,pop 将最后一个元素放到堆顶,然后调用 siftDown 方法,重新安排最小堆

从根元素开始,比较根元素和左右子元素的大小,如果根元素比左右子元素都大,那么就交换根元素和左右子元素中较小的那个元素的位置

一个元素的子节点的索引:

  • left = index * 2
  • right = left + 1

因为这里采用的是用数组存放二叉堆,所以元素索引的方式:

  • left = (index + 1) * 2 - 1
  • right = left + 1

这里为什么用 length >>> 1,是因为非叶子节点的个数恰好等于 length / 2 向下取整

遍历条件通过判断父元素索引是否小于 length / 2 来实现最少遍历次数

比如说当前的 length13,要取出堆顶元素

  • index = 0, length = 12, halfLength = 6
    • leftIndex = 1, rightIndex = 2
    • 如果 leftnode 小, index = 1
  • index = 1, length = 12, halfLength = 6
    • leftIndex = 3, rightIndex = 4
    • 如果 leftnode 小, index = 3
  • index = 3, left = 12, halfLength = 6
    • leftIndex = 7, rightIndex = 8
    • 如果 leftnode 小, index = 7
  • index = 7, left = 24, halfLength = 6 ,循环结束

通过三次遍历就能找到将一个元素放合适位置了

function pop<T: Node>(heap: Heap<T>): T | null {
  if (heap.length === 0) {
    return null;
  }
  const first = heap[0];
  const last = heap.pop();
  if (last !== first) {
    heap[0] = last;
    siftDown(heap, last, 0);
  }
  return first;
}
function siftDown<T: Node>(heap: Heap<T>, node: T, i: number): void {
  let index = i;
  const length = heap.length;
  const halfLength = length >>> 1;
  while (index < halfLength) {
    const leftIndex = (index + 1) * 2 - 1;
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1;
    const right = heap[rightIndex];

    if (compare(left, node) < 0) {
      if (rightIndex < length && compare(right, left) < 0) {
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      } else {
        heap[index] = left;
        heap[leftIndex] = node;
        index = leftIndex;
      }
    } else if (rightIndex < length && compare(right, node) < 0) {
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      return;
    }
  }
}

compare 函数

compare 函数的实现如下:

  • sortIndex 是任务的优先级,id 是任务的唯一标识
  • 优先级相同的情况下,再比较 id

compare(a, b) > 0 说明 a 任务的优先级更高,b 任务的优先级更低,反之亦然。

function compare(a: Node, b: Node) {
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}

小技巧

用数组存储最小堆,一共需要遍历多少次就可以找到目标元素:通过 Math.floor(Math.log2(length - 1)) 计算

延时任务调度和非延时任务调度

延时任务调度使用 requestHostTimeout,非延时任务调度使用 requestHostCallback

  • requestHostTimeout:设置了一个回调函数在指定的延迟时间后被调用,用于在一定的时间过去后执行回调函数
  • requestHostCallback:设置了一个回调函数在浏览器下一个帧被调用,用于在下一帧执行回调函数

requestHostTimeout

这函数是一个 setTimeout,在到达延时时间后执行 callback

源码简化:

const requestHostTimeout = (callback, ms) => {
  setTimeout(() => {
    callback(getCurrentTime());
  }, ms);
};

延时任务回调函数 handleTimeout

handleTimeout 是在 requestHostTimeout 执行是传入的回调函数

执行过程:

  1. 将延迟队列任务中过期任务放到任务列表中
  2. 如果任务列表中有任务,调用 requestHostCallback(flushWork) 执行任务
  3. 如果任务列表中没有任务,判断延迟队列中是否有任务,如果有,调用 requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime) 设置延迟任务

总的来说延迟任务的执行逻辑就是:如果任务列表中有任务,执行任务,如果没有,判断延迟队列中是否有任务,如果有,设置延迟任务

简单说:将延时队列中的过期任务放到非延时队列中,然后调用 requestHostCallback 执行非延时任务

function handleTimeout(currentTime: number) {
  isHostTimeoutScheduled = false;
  advanceTimers(currentTime);

  if (!isHostCallbackScheduled) {
    if (peek(taskQueue) !== null) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

requestHostCallback

执行过程:

  1. requestHostCallback 执行时,调用 schedulePerformWorkUntilDeadline
  2. schedulePerformWorkUntilDeadline 被调用了,会执行 port2.postMessage(null)
  3. port2.postMessage 执行时, port1.onmessage 将会被调用
  4. port1.onmessage 函数体是 performWorkUntilDeadline,所以 performWorkUntilDeadline 会被执行

react 为什么使用使用 MessageChannel 而不是 setTimeoutsetTimeout 是个托底方案):

  1. 因为 setTimeout 是基于时间的,如果浏览器被挂起(例如,当用户切换到其他标签或最小化窗口时),setTimeout 也会被挂起,而 MessageChannel 不会
  2. 还有 setTimeout 的精度也不够,可能存在一定的误差
  3. 然后当有大量的任务需要执行时,setTimeout 会产生很多的定时器,从而影响性能

源码简化:

let scheduledHostCallback = null;
let schedulePerformWorkUntilDeadline;

const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    const hasMoreWork = scheduledHostCallback();
    if (hasMoreWork) {
      schedulePerformWorkUntilDeadline();
    }
  }
};

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
  port.postMessage(null);
};

const requestHostCallback = (callback) => {
  scheduledHostCallback = callback;
  schedulePerformWorkUntilDeadline();
};

非延时任务回调函数 flushWork

调用 workLoop 函数,workLoop 函数会根据任务的优先级和过期时间,以及浏览器的空闲时间,决定是否继续执行下一个任务,还是暂停执行并交还控制权给浏览器

源码简化:

function flushWork(hasTimeRemaining: boolean, initialTime: number) {
  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    return workLoop(hasTimeRemaining, initialTime);
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
  }
}

wookLoop

wookLoop 函数在浏览器空闲时执行 taskQueue

执行逻辑:

  1. 如果当前任务的过期时间(expirationTime)大于当前时间(currentTime),并且没有剩余的时间(!hasTimeRemaining)或者应该让出控制权(shouldYieldToHost),就跳出循环(注释 ①)
  2. 如果任务没有过期,并且还有剩余时间(或者不需要让出控制权),它会执行当前任务的回调函数,并传入一个布尔值表示是否超时(注释 ②)
    • const continuationCallback = callback(didUserCallbackTimeout);
  3. 如果回调函数返回了一个新的函数,说明当前任务还没有完成,需要继续执行,那么它会把新的函数赋值给当前任务的回调,并返回 true 表示还有未完成的任务(注释 ③)
  4. 如果回调函数没有返回新的函数,说明当前任务已经完成,那么它会从任务队列中移除当前任务(注释 ④)
  5. 最后,重复上述步骤,直到任务队列为空或者遇到需要暂停或者让出的情况

源码简化:

function workLoop(hasTimeRemaining: boolean, initialTime: number) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      // ① 表示当前任务过期时间大于当前时间
      currentTask.expirationTime > currentTime &&
      // hasTimeRemaining 表示有没有剩余时间
      // shouldYidldToHost() 表示需要让出给主机
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      break;
    }
    // 任务没有过期并且还有剩余时间(或者不需要让出控制权)

    const callback = currentTask.callback;
    if (typeof callback === "function") {
      // 将当前任务的回调函数清空
      currentTask.callback = null;
      // 设置当前的优先级
      currentPriorityLevel = currentTask.priorityLevel;
      // 任务是否超时
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      // ② 执行当前任务回调函数传入是否超时,并返回一个函数
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      // ③ 如果返回了一个函数,说明当前任务还没有完成,需要继续执行
      if (typeof continuationCallback === "function") {
        currentTask.callback = continuationCallback;
        advanceTimers(currentTime);
        return true;
      } else {
        // ④ 没返回函数,说明当前任务已经完成,从任务队列中移除
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
        advanceTimers(currentTime);
      }
    } else {
      // 如果任务没有回到函数,就从任务队列中移除
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
}

判断 timerQueue 队列中是否有任务过期

timerQueue 队列是用来存放延时任务的

非延迟队列 taskQueue 的优先级比较高,如果一直在执行这个任务队列,可能会导致 timerQueue 中的任务过期

react 通过 advanceTimers 函数,将过期任务从 timerQueue 中取出,放入 taskQueue

执行逻辑:

  1. timer.callback === null 说明这个任务已经被取消了,就用 pop 函数把它从 timerQueue 中移除
  2. timer.startTime <= currentTime 说明这个任务已经可以执行了,就用 pop 函数把它从 timerQueue 中移除,并把它的 sortIndex 设置为 expirationTime,然后用 push 函数把它加入到 taskQueue
function advanceTimers(currentTime: number) {
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
    } else {
      return;
    }
    timer = peek(timerQueue);
  }
}

变量介绍

这些变量的作用是保证 react 在任务时不会出现逻辑错误,我们在学习源码的时候,其实不看这些变量,也不影响理解

不过呢,既然在学习 react 源码了,还是要了解下这些变量存在的意义:

  1. isHostCallbackScheduled
  2. isHostTimeoutScheduled
  3. isPerformingWork

3 个变量都是保证保证调度器的稳定和效率,避免不必要的重复或者中断

isSchedulerPaused 用于暂停下一个任务的调度

也就是说,一个任务如果执行了,是不会被中断或者暂停的

isHostCallbackScheduled

工作原理:

  1. 在调用 flushWork 函数之前,先把 isHostCallbackScheduled 置为 true,然后再 flushWork 调用时将 isHostCallbackScheduled 设置为 false
  2. 如果在 flushWork 函数执行过程中, requestHostCallback 函数被调用了,requestHostCallback 调用说明有新的任务被安排了,那么就需要检查当前是否有任务在执行中
  3. 如果有,就不会安排新的任务(超时函数触发、其他新的任务),而是等待当前任务执行完毕后再安排(等待 flushWork 执行完毕)

isHostTimeoutScheduled

用于标记是否已经设置了一个超时回调函数。如果为 true,表示调度器已经使用 setTimeout 函数安排了一个回调函数,如果为 false,表示调度器还没有安排回调函数

工作原理:

  1. 在调用 handleTimeout 函数之间,先把 isHostTimeoutScheduled 置为 true,然后再 handleTimeout 调用时将 isHostTimeoutScheduled 设置为 false
  2. 如果在 handleTimeout 函数执行过程中,有一个延时任务被安排了,就会调用 cancelHostTimeout 函数,取消当前的定时器,然后重新安排一个定时器,这样就保证了只有一个定时器在运行

isPerformingWork

flushWork 函数执行时,将会标记为 true,表示当前正在执行任务,保证在当前任务执行完成之前不会安排新的任务

在当前任务执行完之后,isPerformingWork 会被置为 false

总结

  1. 任务队列分为延时队列和非延时队列
  2. 延时队列和非延时队列都是最小堆(堆顶的任务优先级最高)
  3. 将延时队列中的过期任务放到非延时队列中,等待执行
    • 延时队列执行时,将过期任务放到非延时队列中
    • 非延时队列执行时,先检查有没过期的延时任务

getNextLanes 函数是如何找到最高优先级的任务

react-reconcilergetNextLanes 函数作用是:

react 一次任务调度中,找出优先级最高的任务
大概分为 4 步:

  1. 根据优先级找出优先级最高的任务
  2. 处理 wipLanesnextLanes
  3. 处理连续输入事件
  4. 处理并发过程中受影响的 lanes

其中 1/3/4 都是会改变 nextLanes2 直接 return wipLanes

React 源码中的一些概念

变量名称中带有 host,一般和宿主有关,在 ReactDOM 中指浏览器相关的东西,比如 HostComponentHostRoot

React 架构介绍

学习 react 源码,有一个绕不过的概念 —— fiberfiber 是啥,仅仅通过代码是很难理解这个概念的

在了解 fiber 之前,先了解一下 react 前置知识

React@15 架构

react@15 的架构:

  • Reconciler:用 diff 算法找出变化的组件
  • Renderer:将变化的组件渲染到页面上

每当更新时,Reconciler 会做这些工作:

  1. 调用组件的 render 方法,生成 VirtualDOM
  2. new VirtualDOMold VirtualDOM 做对比,找出差异
  3. 通知 Renderer 更新页面

Renderer 接收到 Reconciler 的通知后,更新当前页面

由于更新是递归执行的,所以中途不能被打断,需要将调用栈挨个执行完

而且 ReconcilerRenderer 是交替执行的

也就是说一个组件进入 Reconciler 阶段后,用 diff 算法比对完,通知 Renderer 更新页面,再进入下一个组件的 Reconciler,这样一点点渲染

这时如果某个组件的 Reconciler 计算时间过长(> 16.6ms,一秒 60 帧),用户就能在页面中感受到卡顿

这种方案称为 Stack Reconciler,无法打断,一鼓作气运行到底,中途不停歇

React@16 架构

解决这个问题,最直观的方法就是分片,把之前 ReconcilerRenderer 一个完整的任务拆成很多小任务(分片),每个任务执行时的时间都很短(< 16.6ms),这样整个更新的过程就不会独占整个线程了(js 是单线程)

将完整的任务分片之后,这些任务该怎么执行呢?

react@16 对原先的架构进行了重构,增加了 Scheduler,现在架构变为:

  • Scheduler:任务调度,高优先级的进入 Reconciler
  • Reconciler:用 diff 算法找出变化的组件
  • Renderer:将变化的组件渲染到页面上

Scheduler

浏览器提供了一个 requestIdleCallback

这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应

通过这个 api 我们就能知道当前线程是否空闲,决定自己调用相关任务

由于兼容原因,react 没有使用浏览器提供的 api,自己实现了 Scheduler

Reconciler

React@16 版本中 Reconciler 从递归变成了可中断的循环

而且 ReconcilerRenderer 不在交替工作了

Scheduler 把任务交个 Reconciler 后, Reconciler 会为变化的 VirtualDOM 打上标记“增/删/改”

一个任务处在 Reconciler 时,可能会被打断(是否打断,由 Scheduler 调度),打断了之后需要重新执行 Reconciler

Renderer

SchedulerReconciler 工作时,都是在内存中运行的,没有通知 Renderer,所以用户是感知不到的界面变化的

当所有的任务都完成了 Reconciler 之后,才会交给 Renderer 进行渲染

处于 Renderer 阶段时,是不能被打断的,这里会一口气把 DOM 更新完成

fiber 是什么

每一个任务(分片)的数据结构就是 fiber,在 react@16 之后逐渐用 fiber 代替 VirtualDOM

fiber,英文意思是纤维,在计算机中的意思是纤程,它是一种比线程更精细的控制

这里 React Fiberfiber 不是一个概念,react 团队将这种方案命名为 fiber,借用了 fiber 的意思:更紧密的控制

这种方案称为 Fiber Reconciler

组件卸载时 DOM 树的自动清理机制是怎样的

通过上两讲:

  1. 掌握 React 组件树遍历技巧
  2. useEffect 返回的函数是怎么执行的

我们已经知道了 react 是如何找到 passive effect 返回的函数

那么找到这个函数后,怎么执行这个函数呢

我们先来看下面这段代码:

function A() {
  useEffect(() => {
    return () => {
      console.log("执行销毁函数 A");
    };
  }, []);
  useEffect(() => {
    return () => {
      console.log("执行销毁函数 A1");
    };
  }, []);
  return <>文本A</>;
}

一个组件中有两个 passive effect 返回的函数,react 是怎么安排执行的顺序呢?

一个组件中的 passive effect 是用链表的形式存储的

每个 effect 对象都有 destroynext 属性

  • destroy 保存的是 passive effect 返回的函数
  • next 保存的是下一个 effect 对象

最顶层的 effect 是函数组件中写在最上面的 useEffect,通过 next 指向下一个 effect,以此类推,最后一个 effectnext 指向最顶层的 effect

结构如下所示:

let effect = {
  destroy: () => {
    console.log("执行销毁函数 A"));
  },
  next: {
    destroy: () => {
      console.log("执行销毁函数 A1");
    },
    next: {
      destroy: () => {
        console.log("执行销毁函数 A");
      },
      next: { ... },
    },
  },
};

既然是链表,那么执行的顺序就是从最顶层的 effect 开始,依次执行 destroy 函数,最后执行最顶层的 effectdestroy 函数

源码简化:

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) {
          destroy();
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

react 这里使用 do...while 进行遍历,保证所有的 effect 都被执行

释放内存

释放内存分为两个阶段:

  1. 第一个阶段是在向上遍历时
  2. 第二个阶段是在处理完成 deletions

detachFiberAfterEffects

上回说到 react 在处理 deletedNode 时先向下遍历,然后在向上遍历

在向上遍历的过程中会将对应所有遍历到的 fiber 的属性都置为 null,这样可以释放一些内存

function detachFiberAfterEffects(fiber) {
  const alternate = fiber.alternate;
  if (alternate !== null) {
    fiber.alternate = null;
    detachFiberAfterEffects(alternate);
  }
  fiber.child = null;
  fiber.deletions = null;
  fiber.sibling = null;

  if (fiber.tag === HostComponent) {
    const hostInstance = fiber.stateNode;
    if (hostInstance !== null) {
      delete hostInstance[internalInstanceKey];
      delete hostInstance[internalPropsKey];
      delete hostInstance[internalEventHandlersKey];
      delete hostInstance[internalEventHandlerListenersKey];
      delete hostInstance[internalEventHandlesSetKey];
    }
  }
  fiber.stateNode = null;
  fiber.return = null;
  fiber.dependencies = null;
  fiber.memoizedProps = null;
  fiber.memoizedState = null;
  fiber.pendingProps = null;
  fiber.stateNode = null;
  fiber.updateQueue = null;
}

detachAlternateSiblings

当处理完 deletions 时,当前 fiberalternatealternate 下所有的子节点也会被置为 null,这样可以释放一些内存

function detachAlternateSiblings(parentFiber) {
  const previousFiber = parentFiber.alternate;
  if (previousFiber !== null) {
    let detachedChild = previousFiber.child;
    if (detachedChild !== null) {
      previousFiber.child = null;
      do {
        const detachedSibling = detachedChild.sibling;
        detachedChild.sibling = null;
        detachedChild = detachedSibling;
      } while (detachedChild !== null);
    }
  }
}

根节点处理

react 每次遍历都是从根节点开始,那么根节点的处理是怎么样的呢?

在这里 掌握 React 组件树遍历技巧 我们知道 react 是通过调用 commitPassiveUnmountOnFiber 函数来寻找有 passive effectfiber

按照源码去追踪,我们会发现在 recursivelyTraversePassiveUnmountEffects 函数中会调用 commitHookPassiveUnmountEffects 函数,具体解释可以查这里:commitPassiveUnmountOnFiber

源码简化:

function commitPassiveUnmountOnFiber(finishedWork, type) {
  recursivelyTraversePassiveUnmountEffects(finishedWork);
  if (finishedWork.flags & Passive) {
    commitHookPassiveUnmountEffects(
      finishedWork,
      finishedWork.return,
      HookPassive | HookHasEffect
    );
  }
}

react 为什么要多此一举呢?

通过不断的打断点会看到,commitHookPassiveUnmountEffects 函数会被调用两次

recursivelyTraversePassiveUnmountEffects 函数处理的是 finishedWork.chile,而 commitHookPassiveUnmountEffects 函数处理的是 finishedWork

因为 react 是从根节点开始遍历的,所以 commitHookPassiveUnmountEffects 只处理根节点的 passive effect 的返回函数

总结

  1. react 从根组件开始遍历,寻找 passive effectfiber
  2. 在遍历时,会检查每个 fiberdeletions
    • 如果有则暂停 passive effect 的遍历,先处理 deletions
    • 处理完 deletions 后,再继续遍历 passive effectfiber
  3. 在处理 deletions 时,会先向下遍历,然后再向上遍历
    • 向下遍历时,执行 passive effect 的返回函数
    • 向上遍历时
      • 如果遇到 sibling,则会沿着 sibling 向下遍历
      • fiber 的所有属性置为 null,释放内存
      • 直到遇到 deletedNode 结束处理 deletions
  4. 根节点的 passive effect 返回的函数会单独处理

useEffect 返回的函数是怎么执行的(代码片段)

commitPassiveUnmountOnFiber

这段代码的主要逻辑是:在 commit 阶段,卸载 fiber 节点的 passive effect

  1. 如果 fiber 有子节点,那么调用 recursivelyTraversePassiveUnmountEffects,这个函数会递归地卸载子节点的 passive effectrecursivelyTraversePassiveUnmountEffects 函数内部会调用 commitPassiveUnmountOnFiber(注释 ①)
  2. 如果 fiber 节点本身有 Passive 的标志,会调用 commitHookPassiveUnmountEffects 来卸载 passive effect(注释 ②)

刚看到这段代码会很疑惑

因为在 recursivelyTraversePassiveUnmountEffect 函数中调用了 commitHookPassiveUnmountEffects,那为什么这里还要再调用呢?

是因为注释 ② 这段代码只在根节点有效,但是这段代码会一直执行(react 更新是从根节点一层一层往下的)

源码简化:

function commitPassiveUnmountOnFiber(finishedWork) {
  switch (finishedWork.tag) {
    case FunctionComponent: {
      // ① fiber 节点的子节点会走这里
      recursivelyTraversePassiveUnmountEffects(finishedWork);
      // ② fiber 节点本身
      if (finishedWork.flags & Passive) {
        commitHookPassiveUnmountEffects(
          finishedWork,
          finishedWork.return,
          HookPassive | HookHasEffect
        );
      }
      break;
    }
  }
}

深入探究 React 原生事件的工作原理

ReactDOMreactdom 渲染器,它从 react-dom/client 中导出

当我们调用 ReactDOM.createRoot 方法时,createRoot 函数会调用 listenToAllSupportedEvents 方法

listenToAllSupportedEvents 函数定义在 react-dom-bindings/src/events/DOMPluginEventSystem 文件中

当执行 DOMPluginEventSystem 时,react 会进行事件注册:

SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();

可以看到,react 将事件分为五类,分别由不同的事件插件进行处理:

  • SimpleEventPlugin 处理简单事件,例如 onClick
  • EnterLeaveEventPlugin 处理鼠标进出事件,例如 onMouseEnter
  • ChangeEventPlugin 处理修改事件,例如 onChange
  • SelectEventPlugin 处理选择事件,例如 onSelect
  • BeforeInputEventPlugin 处理输入前事件,例如 onBeforeInput

这些事件插件负责将浏览器原生事件转换为 react 事件,并将其分发到正确的组件中

事件注册前准备

下面以 SimpleEventPlugin 为例,讲解事件注册的流程。了解了 SimpleEventPlugin.registerEvents,其他四个事件也就更容易理解了。

虽然这个函数名叫做 registerEvents,但实际上这个函数还没到注册事件的步骤,它只是准备事件名。为此,这个函数会提供三个变量(见【获取所有事件】)。

简单事件

简单事件指只依赖自身的事件,例如 onClick 只依赖 click。

有些事件依赖其他事件,例如 onMouseEnter 依赖 ["mouseout", "mouseover"]react 这么做简化了开发者自己监听事件的步骤。否则,开发者自己实现 onMouseEnter 这种事件,需要分别监听 mouseoutmouseover 事件。

获取所有事件

react 中的事件都是硬编码的,保存在 simpleEventPluginEvents 变量中

const simpleEventPluginEvents = ["click", "mousedown", "mouseenter", ...];

遍历 simpleEventPluginEvents 列表,react 将事件名处理成 domEventNamereactEventName,也就是 react 事件名和 dom 事件名的对应关系。

  • domEventNamedom 事件名全小写,例如 mousedownmouseenter
  • reactEventNamereact 事件名是驼峰形式,例如 onClickonMouseDown

react 中,为了处理 dom 事件,react 定义了一些与原生 dom 事件名对应的 react 事件名。

这些事件名以 on 开头,比如 onClickonFocus 等。同时,react 还定义了一些事件之间的依赖关系,即某些事件需要依赖于其他事件才能正常工作

为了实现这些功能,react 使用了三个变量(详细的事件名在文章底部):

  1. topLevelEventsToReactNames:这个变量是一个 Map,保存着原生 dom 事件名和对应的 react 事件名之间的映射关系。例如,click 事件对应着 onClick 事件。一共有 75 个映射关系
  2. registrationNameDependencies:这个变量是一个普通的对象,保存着 react 事件名和依赖事件名之间的关系。例如,onClick 事件依赖于 click 事件。一共有 166 个依赖关系。
  3. allNativeEvents:这个变量是一个 Set,保存了所有原生 dom 事件名。一共有 81 个事件名。

特殊处理

这里有 7 个事件不在 SimpleEventPluginEvents 变量中,因为它们是需要特殊处理的。

这些事件包括:

  • onAnimationEnd
  • onAnimationIteration
  • onAnimationStart
  • onDoubleClick
  • onFocus
  • onBlur
  • onTransitionEnd

其中,与 AnimationTransition 相关的事件分别是 AnimationEventTransitionEvent

由于浏览器兼容性的问题,react-dom 通过函数 getVendorPrefixedEventName 来实现对它们的兼容性处理(源码)

另外,对于 onDoubleClickonFocusonBlur 这三个事件,它们的 reactEventName 与对应的 domEventName 不同,因此需要特殊处理:

  • onDoubleClick 对应的 domEventNamedbclick
  • onFocus 对应的 domEventNamefocusin
  • onBlur 对应的 domEventNamefocusout

这些细节处理有助于确保 react 应用程序在不同浏览器上的正确运行

事件注册

react 中,事件注册是非常重要的,因为它关系到组件的交互和性能

react 事件注册来自于 listenToAllSupportedEvents 函数

事件注册分为 3 种情况:

  • 绑定在 document
  • 绑定在 div#root
  • 绑定在目标元素 target

react@18 开始,事件绑定在页面根元素中(也就是 div#root),不再绑定在 document

绑定在 document

只有 selectionchange 事件是绑定在 document

绑定在 target

这些事件是不会冒泡的(不需要委托),它们的事件是绑定在事件事件发生的元素身上,这部分事件有普通事件和媒体事件组成,都是硬编码在代码中

绑定在 div#root

allNativeEvents 中排除掉 nonDelegatedEvents 事件,剩下的事件都是绑定在 div#root

注册

在事件注册前判断浏览器是否支持 passive,如果支持,则将 passive 设置为true,浏览器永远不会调用 event.preventDefault(),用于提升性能

这个属性用于 touchstarttouchmovewheel 事件中

然后调用函数分别注册事件,源码

  • addEventBubbleListener:冒泡事件
  • addEventCaptureListener:捕获事件
  • addEventCaptureListenerWithPassiveFlag:捕获事件,passivetrue
  • addEventBubbleListenerWithPassiveFlag: 冒泡事件,passivetrue

在注册事件时事件监听器 react 会经过一系列的处理,最后返回一个 (e) => { .... }

react 处理的这一步,我们叫做合成事件

事件优先级

react 将事件优先级分为 4 种:

  • 离散事件优先级,例如:点击事件,input 输入等触发的更新任务,优先级最高 SyncLane
  • 连续事件优先级,例如:滚动事件,拖动事件等,连续触发的事件 优先级次之,为 InputContinuousLane
  • 默认事件优先级,例如:setTimeout 触发的更新任务,为 DefaultLane
  • 闲置事件优先级,优先级最低,为 IdleLane

react 在事件注册时根据事件名设置不同的优先级,getEventPriority

特殊处理了 message 事件(它的优先级是根据 Scheduler 回调来调度的)

事件队列(合成事件)

这一部分是 react-dom 的核心,react 这么做的目的是为了解决各浏览器之间的差异

事件队列 dispatchQueue 包含两个参数:

  • event:对应的是 react 合成事件,见【收集合成事件】
  • listeners[]:目标节点对应的事件监听器,从目标节点开始,一直到祖先节点,见【收集事件监听器】

收集合成事件

合成事件 SyntheticEventreact 最核心的一部分

提取合成事件也分为 5 个插件:

  • SimpleEventPlugin.extractEvents(...)
  • EnterLeaveEventPlugin.extractEvents(...)
  • ChangeEventPlugin.extractEvents(...)
  • SelectEventPlugin.extractEvents(...)
  • BeforeInputEventPlugin.extractEvents(...)

其中 EnterLeaveEventPluginChangeEventPluginSelectEventPluginBeforeInputEventPlugin 插件的提取事件只在冒泡阶段执行

这些函数内部,react 都会创建一个基本的合成事件,然后再根据事件名分成若干种合成事件

这些合成事件都是由 createSyntheticEvent 函数创建的

react 按照事件名分成了 12 种合成事件:

合成事件有一些基本属性(其他属性对应不同的对应不同的实例,可以点击上面查看源码):

  • _reactNamereact 事件名
  • _targetInsttarget 对应的 fiber
  • type:原生事件类型,比如 click
  • nativeEvent:原生事件信息,event
  • target:原生节点,比如 onClick
  • preventDefaultreact 实现
  • stopPropagationreact 实现
  • persistreact 实现
  • isPersistentreact 实现

除了这些属性之外,reactnativeEvent 中信息都提取到了合成事件中,通过下面这段代码实现

// Interface 是 react 定义的各种合成事件
// nativeEvent 是原生事件
for (const propName in Interface) {
  if (!Interface.hasOwnProperty(propName)) {
    continue;
  }
  const normalize = Interface[propName];
  // normalize 如果存在,说明 propName 对应的属性在合成事件中是一个函数
  // react 这么设计的原因是为了消除不同浏览器之间的差异
  // 见[WheelEventInterface 事件]
  if (normalize) {
    this[propName] = normalize(nativeEvent);
  } else {
    this[propName] = nativeEvent[propName];
  }
}

WheelEventInterface

这里用 WheelEventInterface 事件来说明 react 为什么要定义 normalize 源码

比如 wheel 事件中的 deltaY 属性:

  • webkit 中是 wheelDeltaY
  • IE9 以下是 wheelDelta

react 把这些可能存在兼容问题的属性值写成了函数,没有兼容问题的属性值写成了 0

当代码执行到这里时,react 会根据它自身的判断来达到消除浏览器之间的差异

收集事件监听器

遍历目标节点(target)到祖先节点(div#root),找到所有注册了该事件类型的监听器,将它们存储在一个数组中:

listeners = [targetListener, ..., rootListener]

监听器的属性包括:

  • instancedom 节点对应的 fiber
  • listener:监听器函数
  • currentTarget:目标节点 target

在收集监听器的函数中,react 从目标节点开始向上遍历,instance.return 得到的值是当前节点的父节点

// 目标节点
let instance = targetFiber;
while (instance !== null) {
  // 用 vite 创建的项目
  // stateNode: img,            tag: 5
  // stateNode: a ,             tag: 5
  // stateNode: div,            tag: 5
  // stateNode: div.app,        tag: 5
  // stateNode: null,           tag: 5
  // stateNode: null,           tag: 8
  // stateNode: FiberRootNode,  tag: 3
  // tag 类型是 WorkTag,文件:packages/react-reconciler/src/ReactWorkTags.js
  // stateNode 与 fiber 相关的 dom
  const { stateNode, tag } = instance;

  // 省略若干代码 ...

  const listener = getListener(instance, reactEventName);
  if (listener != null) {
    listeners.push(
      createDispatchListener(instance, listener, lastHostComponent)
    );
  }

  // 省略若干代码 ...

  // 当前节点的父节点
  instance = instance.return;
}

当监听器和合成事件都准备好后,将他们放入 dispatchQueue 队列中

执行事件队列

执行事件队列分为冒泡和捕获阶段

在上一节中我们得到事件监听器是这样存储的

listeners = [targetListener, ..., rootListener]

再执行事件队列时,需要判断当前处于什么阶段:

  • 如果当前是捕获阶段,从根节点开始(也就是从队列的最后一个开始执行)
  • 如果当前是冒泡节点,从目标节点开始(也就是从队列的第一个开始执行)

在执行回调函数时,React 会记录错误并继续执行应用程序,它确保了在 React 组件中发生的错误不会导致整个应用程序崩溃

总结

  1. 在调用 listenToAllSupportedEvents 函数时,react-dom-bindings/src/events/DOMPluginEventSystem 被执行,初始化事件挂载相关的参数
    • topLevelEventsToReactNames
    • registrationNameDependencies
    • allNativeEvents
  2. 事件注册,将事件挂载到对应的节点
  3. 处理事件监听器
    • 合成事件
    • 事件监听器收集
  4. 事件触发时执行事件

一个不理解的地方

事件队列是一个数组 Array<DispatchEntry>

dispatchQueue

我测试了多种事件,dispatchQueue 都没有出现多项的情况,冒泡或者捕获的事件时存放在 listeners 属性中的,所以我不知道在什么情况下会 dispatchQueue 出现多项

react 中事件名映射

  1. allNativeEvents
    allNativeEvents
  2. topLevelEventsToReactNames
    topLevelEventsToReactNames
  3. registrationNameDependencies
    registrationNameDependencies1
    registrationNameDependencies2

react 中的任务(代码片段)

task(任务)

taskreact 中的一个任务,它有下面几个属性:

type Task = {
  id: number;
  callback: Callback | null;
  priorityLevel: PriorityLevel;
  startTime: number;
  expirationTime: number;
  sortIndex: number;
  isQueued?: boolean;
};
  • id: 使用的是一个全局变量 taskIdCounter,每次创建任务的时候都会自动加一,用于区分不同的任务
  • callback:一个函数,表示要执行的任务,它会在调度器安排好时间后被调用
    • 这个取决于你在里面做什么,比如更新组件的状态、渲染组件、或者执行其他逻辑
  • priorityLevel:优先级,会影响任务何时被执行,有五种:ImmediatePriorityUserBlockingPriorityIdlePriorityLowPriorityNormalPriority
  • startTime:任务开始时间,它是使用 getCurrentTime 函数获取的微秒级别的当前时间
  • expirationTime:任务过期的时间,它是根据 startTimepriorityLevel 计算出来的。过期时间越小,表示任务越紧急,越需要优先执行
    • expirationTime 的计算公式为:startTime + timeouttimeout 的值取决于 priorityLevel,它们的对应关系如下:
      • ImmediatePriority: -1 (立即执行)
      • UserBlockingPriority: 250 (250ms 后执行)
      • NormalPriority: 5000 (5s 后执行)
      • LowPriority: 10000 (10s 后执行)
      • IdlePriority: 1073741823 (1073741.823s 后执行)
    • react 这么设计是因为 expirationTime 的值越小,表示任务越紧急,越需要优先执行
  • sortIndex:任务在队列中的排序索引,默认为 -1。它实际上会被设置成 startTimeexpirationTime,以便在插入队列时按照升序排列
    • 保证过期时间越小,越紧急的任务排在最前面
  • isQueued:任务是否已经被加入到队列中。这个属性是为了避免重复插入或删除任务而设置的
    • 在性能分析模式下,会加入这个属性,目前这个值没有启用

IdlePriority 这个优先级的过期时间大概在 12 天,这意味着几乎不会过期,通常用于一些不紧急的后台任务,比如数据预加载、用户不可见的更新等
这个优先级的任务会被延迟到其他更重要的任务都完成后再执行

React dom 遍历逻辑

下面的 dom 结构react 内部是如何遍历的

const App = () => {
  return (
    <div>
      <button>+1</button>
      <A count={0} />
    </div>
  );
};
const A = (props) => {
  useEffect(() => {
    console.log(props.count);
  }, [props.count]);
  return <div>{props.count}</div>;
};

react 内部遍历核心逻辑:

  1. render 时调用 commitPassiveUnmountOnFiber 函数
  2. commitPassiveUnmountOnFiber 处理不同的 WorkTag,并调用 recursivelyTraversePassiveUnmountEffects
  3. recursivelyTraversePassiveUnmountEffects 根据当前 Fiber 的子节点有没有 passive effectuseEffectuseLayoutEffect)来决定是否遍历当前 Fiber 的子节点
    • 如果子节点有 passive effect,则优先遍历子节点 (深度优先),直到找到最终的叶子节点,退出当前循环
    • 然后进入兄弟节点,开始遍历兄弟节点的子节点
      • 具体从哪个兄弟节点开始遍历,react 选择的是离退出循环的那个叶子节点的父节点,检查有没有子节点,以此循环遍历
    • 直到最后找到所有有 passive effect 的节点

代码简化:

commitPassiveUnmountOnFiber(root.current);

function commitPassiveUnmountOnFiber(finishedWork) {
  // 省略了处理不同的 WorkTag
  recursivelyTraversePassiveUnmountEffects(finishedWork);
}

function recursivelyTraversePassiveUnmountEffects(parentFiber) {
  // 省略了其他处理
  if (parentFiber.subtreeFlags & PassiveMask) {
    let child = parentFiber.child;
    while (child !== null) {
      commitPassiveUnmountOnFiber(child);
      child = child.sibling;
    }
  }
}

所以对于这段 dom 的遍历逻辑是:

  1. 首先从根组件开始 FiberRootNode,取到 current
    • 也就是说 FiberRootNode.currentdiv#root 这是一个 fiber,它的 tag3
  2. 由于 App 的子组件有 passive effect,所以会进入 App 组件,它的 tag0
  3. App 组件中节点是 <div><div>tag5
    • <div> 下面有两个子元素 <button><A>
  4. 先遍历 <button> 它的 tag5
  5. <button> 内部只有一个文本节点,没有 passive effect
    • 所以 react 不遍历了(跳出当前遍历的循环,也就是 button 这条不在遍历了)
  6. 跳出循环后,查看 button 的兄弟节点,它的兄弟节点是 <A><A>tag0
  7. 由于 <A> 节点的子节点没有 passive effect,所以跳出循环,结束整个遍历

总结

  1. 从跟节点开始遍历
  2. 当前组件的子组件有没有 passive effect
  3. 采取深度优先
  4. 如果 dom 节点内有函数组件,则这个 dom 会被遍历,否则不会遍历
  5. 如果当前 fiber 下的所有子 fiber 都没有 passive effect ,则这一整个都链表都不会被遍历
  6. 如果当前 fiber 只有 dom,则这些 dom 也不会遍历

总的来说组件会不会别遍历看 fiber 有没有 passive effect

  • 有,一定会被遍历
  • 没有,下面两种情况会被遍历,其他情况不会被遍历
    • passive effect 的父组件
    • passive effect 组件是兄弟组件

passive effect 指的是 useEffectuseLayoutEffect

遍历逻辑如下图所示

图中画绿色勾的都会被遍历,红色勾是遍历的顺序

react 遍历逻辑

一些看源码的感想

今年是我做前端第五年了,我才第一次看源码,才看了几天,也就是这几天让我感受很大,后悔没有早点看源码

其实也不能说之前没有看源码,只是说之前看源码是通过别人讲解,比如手写 react 这种,虚拟 dom 工作原理这种

通过这种形式了解源码,我现在感觉意义不大,因为一个功能的实现有好几种,他和你讲的方式不一定是源码的实现方式

你听他讲完了之后,你了解了原理,然后你就认为源码也是这样实现的(有些厉害点的人可能会想到,源码会处理些边界问题)

但是呢,你没有看源码,你就只能停留在这一步,你根本不知道源码在背后做了哪些事情

还有一些大 v 会说,我是不看源码的,遇到问题了才去看

基本说这种话的人,源码都不知道看了多少了,只是当下用的技术的源码没有去看,他敢说,我从写代码以来没有看过任何源码吗?

因为设计模式,数据结构,算法这些是不变的,所以各种框架/库的诞生,用作者理解的方式去解决现有的问题

当你有了积累,在出现新的技术后,你也能快速明白它的工作原理,不再需要去看源码了

下面是我看源码的一些方式,用最笨的方法:

react 使用 flow 写的,没法直接调试,我看了网上的几种调试方式:

  1. react 打包后的代码调试,这种方法在调试时无法进入源码,离源码就会有距离
  2. 在创建一个 react 下面,在这个项目下 clone 一份 react 源码,通过配置,让项目中的 react 找到源码中的 react

我就是用第 2 种方式学习的,只是第二种方式需要将 flow 项目的代码删除,启动项目的时候就能在源码中调试了

阅读没有 flow 的源码时,会有很多心智负担,类型可以帮你理解代码中的边界,取值的范围,所以删除了 flow 的源码不建议阅读

那怎么办呢?有 flow 的代码没法调试,没有 flow 的代码不适合阅读

那就结合呗,一份用来阅读,一份用来调试,当你看不懂时,用调试走一走这块的逻辑

如何配置调试项目,具体可以看:React 源码调试方式

环境准备好之后,就要开始阅读源码了,在阅读源码的时候一定要做笔记

  1. 画出函数之间跳转的流程图
  2. 在函数/变量上写好各个阶段它的取值
  3. 如果有精力,将 1、2 两步整理成笔记

因为 react 有很多文件,而且函数拆的很细,一些变量/函数的名字换来换去,文件之间跳来跳去,你不记录,很快你就不知道要干嘛了

网上很多人说,看源码的时候不要在意细节,我认为这种方式不适合第一次阅读源码的人

我说的第一次阅读源码指的是之前从没有看过源码,这次第一次阅读源码

第一次看源码,你可能不了解设计模式,数据结构,算法,还有一些名词,如果你不在意这些细节,后面的代码你根本看不下去

所以第一次看源码的人,一些你不了解的名词,要去网上查一查,了解它是干什么的,代码中的的注释都要认真去看

最后阅读源码特别废精力,不是短时间就能完成的,时间长了就会懈怠

每次想看又不想看的时候跟自己说,就看五分钟,如果五分钟后能进入状态,就接着看下去,如果五分钟还是看不下去,就果断放下去干你想干的事情

React 源码调试方式

  1. 使用 pnpm create vite 创建 react 项目

  2. src 下面下载 react 源码

    # [email protected]
    git clone https://github.com/facebook/react.git
  3. 修改 vite 配置,reactreact-domreact-dom-bindingsreact-reconcilerschedulersharedreact 的核心包,修改 alias 让它用 src/react 下的包

    import path from "path"
    resolve: {
      alias: {
        react: path.posix.resolve("src/react/packages/react"),
        "react-dom": path.posix.resolve("src/react/packages/react-dom"),
        "react-dom-bindings": path.posix.resolve(
          "src/react/packages/react-dom-bindings"
        ),
        "react-reconciler": path.posix.resolve(
          "src/react/packages/react-reconciler"
        ),
        scheduler: path.posix.resolve("src/react/packages/scheduler"),
        shared: path.posix.resolve("src/react/packages/shared"),
      },
    },
  4. vite 环境配置

    define: {
     __DEV__: true,
     __EXPERIMENTAL__: true,
     __PROFILE__: true,
    },
  5. 删除 flow 相关的代码

    pnpm i flow-remove-types prettier -g
    
    # 删除 flow 注解
    flow-remove-types --out-dir src/react src/react
    # 格式化代码
    prettier src/react --ignore-unknown --write
  6. 修改 src/main.jsxReactReactDOM 的导入方式

    import * as React from "react";
    import * as ReactDOM from "react-dom/client";
  7. src/react/packages/react-reconciler/src/ReactFiberHostConfig.js 文件中修改。如果没生效,删除 /node_modules/@vitejs,重启服务

    // 报错 Uncaught SyntaxError: The requested module '/src/react/packages/react-reconciler/src/ReactFiberHostConfig.js?t=1676712542480' does not provide an export named 'getChildHostContext'
    
    // 注释
    // throw new Error('This module must be shimmed by a specific renderer.');
    // 修改为
    export * from "react-dom-bindings/src/client/ReactDOMHostConfig";
  8. src/react/packages/shared/ReactComponentStackFrame.js 文件中修改。如果没生效,删除 /node_modules/@vitejs,重启服务

    // 报错 ReactComponentStackFrame.js:27 Uncaught TypeError: Cannot destructure property 'ReactCurrentDispatcher' of 'ReactSharedInternals_default2' as it is undefined.
    
    // 注释
    // import ReactSharedInternals from 'shared/ReactSharedInternals';
    // 修改为
    import ReactSharedInternals from "../react/src/ReactSharedInternals";
  9. 配置 jsconfig.jsonvscode 使用

    {
      "compilerOptions": {
        "baseUrl": "./",
        "paths": {
          "react/*": ["src/react/packages/react/*"],
          "react-dom/*": ["src/react/packages/react-dom/*"],
          "react-dom-bindings/*": ["src/react/packages/react-dom-bindings/*"],
          "react-reconciler/*": ["src/react/packages/react-reconciler/*"],
          "scheduler/*": ["src/react/packages/scheduler/*"],
          "shared/*": ["src/react/packages/shared/*"]
        }
      },
      "exclude": ["node_modules", "dist"]
    }
  10. vscode 增加调试文件 vscode/launch.json

    {
      "version": "0.2.0",
      "configurations": [
        {
          "type": "chrome",
          "request": "launch",
          "name": "针对 localhost 启动 Chrome",
          "url": "http://localhost:5173",
          "webRoot": "${workspaceFolder}"
        }
      ]
    }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.