美文网首页react & vue & angular
60行代码实现React的事件系统

60行代码实现React的事件系统

作者: 涅槃快乐是金 | 来源:发表于2022-04-15 19:54 被阅读0次

    原文:https://segmentfault.com/a/1190000041355419

    大家好,我卡颂。

    由于如下原因,React的事件系统代码量很大:

    • 需要抹平不同浏览器的差异
    • 与内部的优先级机制绑定
    • 需要考虑所有浏览器事件

    但如果抽丝剥茧会发现,事件系统的核心只有两个模块:

    • SyntheticEvent(合成事件)
    • 模拟实现的事件传播机制

    本文会用60行代码实现这两个模块,让你快速了解React事件系统的原理。

    在线DEMO地址

    欢迎加入人类高质量前端框架群,带飞

    Demo的效果

    对于如下这段JSX

    const jsx = (
      <section onClick={(e) => console.log("click section")}>
        <h3>你好</h3>
        <button
          onClick={(e) => {
            // e.stopPropagation();
            console.log("click button");
          }}
        >
          点击
        </button>
      </section>
    );
    

    在浏览器中渲染:

    const root = document.querySelector("#root");
    ReactDOM.render(jsx, root);
    

    点击按钮,会依次打印:

    click button
    click section
    

    如果在button的点击回调中增加e.stopPropagation(),点击后会打印:

    click button

    我们的目标是将JSX中的onClick替换为ONCLICK,但是点击后的效果不变。

    也就是说,我们将基于React自制一套事件系统,他的事件名的书写规则是形如ONXXX全大写形式。

    实现SyntheticEvent

    首先,我们来实现SyntheticEvent(合成事件)。

    SyntheticEvent是浏览器原生事件对象的一层封装。兼容所有浏览器,同时拥有和浏览器原生事件相同的API,如stopPropagation()preventDefault()

    SyntheticEvent存在的目的是抹平浏览器间在事件对象间的差异,但是对于不支持某一事件的浏览器,SyntheticEvent并不会提供polyfill(因为这会显著增大ReactDOM的体积)。

    我们的实现很简单:

    class SyntheticEvent {
      constructor(e) {
        this.nativeEvent = e;
      }
      stopPropagation() {
        this._stopPropagation = true;
        if (this.nativeEvent.stopPropagation) {
          this.nativeEvent.stopPropagation();
        }
      }
    }
    

    接收原生事件对象,返回一个包装对象。原生事件对象会保存在nativeEvent属性中。

    同时,实现了stopPropagation方法。

    实际的SyntheticEvent会包含更多属性和方法,这里为了演示目的简化了

    实现事件传播机制

    事件传播机制的实现步骤如下:

    1. 在根节点绑定事件类型对应的事件回调,所有子孙节点触发该类事件最终都会委托给根节点的事件回调处理。
    2. 寻找触发事件的DOM节点,找到其对应的FiberNode(即虚拟DOM节点)
    3. 收集从当前FiberNode到根FiberNode之间所有注册的该事件对应回调
    4. 反向遍历并执行一遍所有收集的回调(模拟捕获阶段的实现)
    5. 正向遍历并执行一遍所有收集的回调(模拟冒泡阶段的实现)

    首先,实现第一步:

    // 步骤1
    const addEvent = (container, type) => {
      container.addEventListener(type, (e) => {
        // dispatchEvent是需要实现的“根节点的事件回调”
        dispatchEvent(e, type.toUpperCase(), container);
      });
    };
    

    在入口处注册点击回调

    const root = document.querySelector("#root");
    ReactDOM.render(jsx, root);
    // 增加如下代码
    addEvent(root, "click");
    

    接下来实现根节点的事件回调

    const dispatchEvent = (e, type) => {
      // 包装合成事件
      const se = new SyntheticEvent(e);
      const ele = e.target;
    
      // 比较hack的方法,通过DOM节点找到对应的FiberNode
      let fiber;
      for (let prop in ele) {
        if (prop.toLowerCase().includes("fiber")) {
          fiber = ele[prop];
        }
      }
    
      // 第三步:收集路径中“该事件的所有回调函数”
      const paths = collectPaths(type, fiber);
    
      // 第四步:捕获阶段的实现
      triggerEventFlow(paths, type + "CAPTURE", se);
    
      // 第五步:冒泡阶段的实现
      if (!se._stopPropagation) {
        triggerEventFlow(paths.reverse(), type, se);
      }
    };
    

    接下来收集路径中该事件的所有回调函数

    收集路径中的事件回调函数

    实现的思路是:从当前FiberNode一直向上遍历,直到根FiberNode。收集遍历过程中的FiberNode.memoizedProps属性内保存的对应事件回调

    const collectPaths = (type, begin) => {
      const paths = [];
    
      // 不是根FiberNode的话,就一直向上遍历
      while (begin.tag !== 3) {
        const { memoizedProps, tag } = begin;
    
        // 5代表DOM节点对应FiberNode
        if (tag === 5) {
          const eventName = ("on" + type).toUpperCase();
    
          // 如果包含对应事件回调,保存在paths中
          if (memoizedProps && Object.keys(memoizedProps).includes(eventName)) {
            const pathNode = {};
            pathNode[type.toUpperCase()] = memoizedProps[eventName];
            paths.push(pathNode);
          }
        }
        begin = begin.return;
      }
    
      return paths;
    };
    

    得到的paths结构类似如下:

    image.png

    捕获阶段的实现

    由于我们是从目标FiberNode向上遍历,所以收集到的回调的顺序是:

    [目标事件回调, 某个祖先事件回调, 某个更久远的祖先回调 ...]
    

    要模拟捕获阶段的实现,需要从后向前遍历数组并执行回调。

    遍历的方法如下:

    const triggerEventFlow = (paths, type, se) => {
      // 从后向前遍历
      for (let i = paths.length; i--; ) {
        const pathNode = paths[i];
        const callback = pathNode[type];
    
        if (callback) {
          // 存在回调函数,传入合成事件,执行
          callback.call(null, se);
        }
        if (se._stopPropagation) {
          // 如果执行了se.stopPropagation(),取消接下来的遍历
          break;
        }
      }
    };
    

    注意,我们在SyntheticEvent中实现的stopPropagation方法,调用后会阻止遍历的继续。

    冒泡阶段的实现

    有了捕获阶段的实现经验,冒泡阶段很容易实现,只需将paths反向后再遍历一遍就行。

    总结

    React事件系统的核心包括两部分:

    • SyntheticEvent
    • 事件传播机制

    事件传播机制由5个步骤实现。

    总的来说,就是这么简单。

    相关文章

      网友评论

        本文标题:60行代码实现React的事件系统

        本文链接:https://www.haomeiwen.com/subject/fdmqertx.html