React 合成事件的设计原理 3

著有《React 源码》《React 用到的一些算法》《javascript地月星》等多个专栏。欢迎关注。

文章不好写,要是有帮助别忘了点赞,收藏~ 你的鼓励是我继续挖干货的的动力🔥。

另外,本文为原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解~

总流程

合成事件的设计原理:

  1. 给容器绑定统一的事件监听器
  2. 创建合成事件对象
  3. 收集Fiber事件 (详细)
  4. 事件回调的派发

这一篇处在第3点,收集Fiber事件。

树"歪"了 以一个HostPortal场景举例

jsx 复制代码
function Modal(){return ReactDOM.createPortal(<div>...</div>,document.getElementById('modal-container'))}
function AppA(){return <div><Modal/></div>}
function AppB(){return <div><div id="modal-container"></div></div>}

ReactDOM.createRoot(document.getElementById('rootA')).render(<AppA />); //Root A
ReactDOM.createRoot(document.getElementById('rootB')).render(<AppB />); //Root B
html 复制代码
index.html
<body>
    <div id="rootA" class="app-container"></div>
    <div id="rootB" class="app-container"></div>
</body>

Fiber树(逻辑树)

js 复制代码
HostRoot rootA
   |-- AppA
        |-- div
             |-- Modal
                  |-- HostPortal
                          |-- div
                               |-- button(点击事件源)

HostRoot rootB
   |-- AppB
        |-- div
             |-- div div#modal-container

DOM树(真实树)

js 复制代码
<body>
   |-- div#rootA
   |    |-- <一些HTML元素>
   |
   |-- div#rootB
         |-- div 
             |-- <一些HTML元素>
             |-- div#modal-container
                    |-- div
                         |-- button (点击事件源)

DOM树和Fiber树是不一致的!

HostPortal内的Fiber节点,Modal、div...都在应用A的Fiber树。但是Portal渲染的DOM,都添加在应用B的DOM树。

也就是树"歪"了。

假如在页面上点击了button事件,事件流会是什么样的?

这就是进入我们的正文,事件的收集。

dispatchEventForPluginEventSystem

js 复制代码
function dispatchEventForPluginEventSystem(DOMEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
  var ancestorInst = targetInst;

  if ((eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 && (eventSystemFlags & IS_NON_DELEGATED) === 0) {
    var targetContainerNode = targetContainer; 
    
    if (targetInst !== null) {
      // The below logic attempts to work out if we need to change
      // the target Fiber to a different ancestor. We had similar logic
      // in the legacy event system, except the big difference between
      // systems is that the modern event system now has an event listener
      // attached to each React Root and React Portal Root. Together,
      // the DOM nodes representing these roots are the "rootContainer".
      // To figure out which ancestor instance we should use, we traverse
      // up the Fiber tree from the target instance and attempt to find
      // root boundaries that match that of our current "rootContainer".
      // If we find that "rootContainer", we find the parent Fiber
      // sub-tree for that root and make that our ancestor instance.
      var node = targetInst;

      mainLoop: while (true) {
        if (node === null) {
          return;
        }

        var nodeTag = node.tag;

        if (nodeTag === HostRoot || nodeTag === HostPortal) {
          var container = node.stateNode.containerInfo;

          if (isMatchingRootContainer(container, targetContainerNode)) {
            break;
          }

          if (nodeTag === HostPortal) {
            // The target is a portal, but it's not the rootContainer we're looking for.
            // Normally portals handle their own events all the way down to the root.
            // So we should be able to stop now. However, we don't know if this portal
            // was part of *our* root.
            var grandNode = node.return;

            while (grandNode !== null) {
              var grandTag = grandNode.tag;

              if (grandTag === HostRoot || grandTag === HostPortal) {
                var grandContainer = grandNode.stateNode.containerInfo;

                if (isMatchingRootContainer(grandContainer, targetContainerNode)) {
                  // This is the rootContainer we're looking for and we found it as
                  // a parent of the Portal. That means we can ignore it because the
                  // Portal will bubble through to us.
                  return;
                }
              }

              grandNode = grandNode.return;
            }
          } 
          // Now we need to find it's corresponding host Fiber in the other
          // tree. To do this we can use getClosestInstanceFromNode, but we
          // need to validate that the Fiber is a host instance, otherwise
          // we need to traverse up through the DOM till we find the correct
          // node that is from the other tree.
          while (container !== null) {
            var parentNode = getClosestInstanceFromNode(container);

            if (parentNode === null) {
              return;
            }

            var parentTag = parentNode.tag;

            if (parentTag === HostComponent || parentTag === HostText) {
              node = ancestorInst = parentNode;
              continue mainLoop;
            }

            container = container.parentNode;
          }
        }

        node = node.return;
      }
    }
  }

  batchedUpdates(function () {
    return dispatchEventsForPlugins(DOMEventName, eventSystemFlags, nativeEvent, ancestorInst);
  });
}
两个回溯
  1. 本树回溯:本树上对container和targetContainerNode进行配对。if(isMatchingRootContainer()){} if (nodeTag === HostPortal) {}

  2. 跨树回溯:若本树无匹配,则找到HostPortal的容器DOM。借助HostPortal的容器DOM跳转到另一颗Fiber树继续回溯。while (container !== null) {}

targetContainerNode targetInst ancestorInst
  • targetContainerNode是HostRoot、HostPortal的容器。例如前面例子中的div#root和div#modal-container。
  • targetInst是事件发生的DOM元素对应的Fiber实例。例如例子中的button的Fiber。

一旦某棵Fiber树的HostRoot HostPortal的containerInfotargetContainerNode匹配,表明targetContainerNode在这棵Fiber树上。

  • 确定了在这棵树上,就可以更新ancestorInst

若在本棵树上找到匹配,则ancestorInst=targetInst。targetInst是事件发生的DOM节点,它的Fiber。

若跨树了,则node = ancestorInst = parentNode。parentNode是getClosestInstanceFromNode(container)。

回溯的流程

到目前为止,可能还不知道回溯在干嘛、更新ancestorInst在干嘛,其实观察例子就可以知道:一开始事件在button触发,原生事件在div#rootB DOM树上冒泡,直到被容器div#rootB捕获,触发React的Fiber树回溯,

首先在本树上回溯,就是Fiber A树,因为targetInst是button的Fiber, 它在Fiber A树上。targetContainerNode是div#rootB。

观察Fiber A树,就是从button向上遍历到HostPortal,从HostPortal到HostRoot A。

到顶也不能匹配,进入到第二部分的逻辑。

从button向上遍历到HostPortal,从HostPortal到HostRoot A 后没有匹配后,通过HostPortal,找到它的DOM容器div#modal-container,通过容器的Fiber向上遍历到HostRoot B,匹配。

观察Fiber A树,就是从button到HostPortal,

观察div#rootB DOM树,找到HostPortal的容器div#modal-container,

观察Fiber B树,找到容器的Fiber div#modal-container,从Fiber div#modal-container向上遍历到HostRoot B。匹配。(Fiber B树回溯)

jsx 复制代码
//第二部分的逻辑:
<div id="rootA"></div>
<div id="rootB">
  <div id="modal-container">3.我是容器,找到我的Fiber
    <div>
      <button></button> <--点击
    </div>
  </div>
</div>
  
<AppA>
  <HostPortal>2.找我的容器,
    <div>
        <button></button>1.从这个targinInst开始在Fiber A树遍历
    </div>
  </HostPortal>  
</AppA>
  
<AppB> 5.匹配
  <div id="modal-container"></div>4.我是容器的Fiber,现在在Fiber B树遍历
</AppB>

回溯添加的次数

其实这个例子一共有3个回溯:

  • targetInst=Fiber button, targetContainerNode='div#rootA' 不需要跨树
  • targetInst=Fiber button, targetContainerNode='div#modal-container' 不需要跨树
  • targetInst=Fiber button, targetContainerNode='div#rootB' 需要跨树,上面例子就是这种

为什么要这样?

想象一下,如果这个例子中不单单在HostPortal上绑定了点击事件,还在HostPortal的父节点,还在HostRoot A、B上都绑定了点击事件,这样就能收集到全部的事件。

但是这个例子的点击事件只会触发后面2个,因为事件是沿着div#rootB DOM树冒泡的,冒泡到div#modal-container触发一次回溯,冒泡到div#rootB触发第二次回溯。

事件收集的起点

回溯的作用是更新ancestorInst,在dispatchEventsForPlugins中Fiber树从ancestorInst开始向上遍历收集事件。

targetContainerNode = 'div#rootA'

targetContainerNode = 'div#modal-container'

targetContainerNode = 'div#rootB'

第一个从Fiber button到HostRoot A收集一次 (不触发)

第二个ancestorInst是Fiber button,进入到dispatchEventsForPlugins,

收集Fiber A树,从button到HostPortal,还会继续收集从HostPortal到HostRoot A的点击事件,

没有在到达HostPortal就停下来,而是继续收集了HostPortal到HostRoot A的事件。

第三个一开始ancestorInst是Fiber button,跨树回溯,更新为Fiber div#modal-container,

进入到dispatchEventsForPlugins,只收集Fiber B树从Fiber div#modal-container到HostRoot B的点击事件。

js 复制代码
第二个:
HostRoot rootA  继续向上 收集事件A
   |-- AppA  继续向上 收集事件A
        |-- div  继续向上 收集事件A
             |-- Modal  继续向上 收集事件A
                  |-- HostPortal 收集事件A
                          |-- div  收集事件A
                               |-- button(点击事件源)  收集事件A
第三个:
HostRoot rootB 收集事件B
   |-- AppB 收集事件B
        |-- div 收集事件B
             |-- div div#modal-container 收集事件B

点击关闭,先收集了A, 后收集了B :

getClosestInstanceFromNode(container)

getInstanceFromNode + Closest 通过DOM Node实例获取Fiber实例,加上Closest就是 最近的祖先节点。

返回值是

  1. 在SSR+Suspense的场景,返回注释边界
  2. 没有在SSR+Suspense的场景,则直接返回自身的Fiber

对于1,这种情况,因为不是if (parentTag === HostComponent || parentTag === HostText) {}所以要继续向上,是注释的父级。

html 复制代码
<div> <-- ancestorInst最终确定为这个的Fiber
  <!-- $ --> <-- 返回这个
  <div id="modal-container"> <-- 没有水合,没有被管理
    Loading...
  </div>
  </!-- /$ -->
</div>

但是如果已经水合了,ancestorInst就是Fiber div#modal-container本身。

对于2,就是我们例子的情况。container是某个HostRoot HostPortal的容器DOM(containerInfo),它有Fiber,该Fiber可以直接返回。

例如div#modal-container是HostPortal的containerInfo,返回div#root的Fiber。

isMatchingRootContainer

ini 复制代码
function isMatchingRootContainer(grandContainer, targetContainer) {
  return (
    grandContainer === targetContainer || //当前container与targetContainer配对
    (grandContainer.nodeType === COMMENT_NODE &&
     grandContainer.parentNode === targetContainer)
  );
}

条件1:当前container 与 HostRoot或HostPortal是否配对。

条件2:注释的父节点与HostRoot或HostPortal是否配对

if (nodeTag === HostRoot || nodeTag === HostPortal){}已经限制死了只能有HostRoot HostPortal,不会用到条件2。

xml 复制代码
<div id="root">
  <!--$-->
  <div>App 内容</div>
  <!--/$-->
</div>

总结

React的事件绑定在Fiber节点上,事件收集就是收集Fiber节点上的事件,批量执行。

在收集前,要知道收集的Fiber起点,

容器DOM上给所有的可以委托的事件绑定了统一的事件监听器,触发dispatchEventForPluginEventSystem通过回溯的方式更新ancestorInst,作为事件收集的Fiber起点。

第一步:事件冒泡的机制,原生事件沿着DOM树冒泡,到达容器DOM。

第二步:触发绑定在容器DOM的事件,执行dispatchEventForPluginEventSystem。确定时间收集的起点,收集Fiber路径上的事件回调(可以简单的理解dispatchEventsForPlugins就是后续的事件收集)。

第三步:批量执行事件回调。

相关推荐
牛仔只喝牛奶17 小时前
多端跨端技术选型:小程序 + H5 + App 的两栈拆分落地
react native·react.js
wear工程师21 小时前
setState 到底是不是异步的?这题从 React 16 问到 18,标准答案早就变了
react.js·面试
wanghowie1 天前
LangGraph4j 落地(二)— ReAct 多步工具与 trace 扩展
前端·react.js·前端框架
csj501 天前
前端基础之《React(11)—state声明式》
前端·react.js
张元清1 天前
React useLocalStorage Hook:SSR 安全的持久化状态(2026)
javascript·react.js
析数塔1 天前
2026前端框架深度解析:React Compiler、Angular Zoneless、Vue 3.5重写游戏规则
前端·vue.js·react.js
杖雍皓1 天前
没有服务器如何构建现代 Web 应用:平台选型与组合策略
运维·服务器·前端·react.js·github·netlify·vercel
sun_weitao1 天前
微前端框架之qiankun 怎么用?vue3+vite+qiankun 和 react + qiankun
前端·react.js·前端框架
名字还没想好☜2 天前
React useEffect 依赖数组踩坑:闭包陷阱与清理函数
前端·javascript·react.js·react·useeffect
我是大卫2 天前
【图】React源码解析-第四部分:事件与跨层传递
react.js·源码