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

上一篇 / 下一篇  2022-02-09 09:46:50

  由于如下原因,React的事件系统代码量很大:
  · 需要抹平不同浏览器的差异
  · 与内部的「优先级机制」绑定
  · 需要考虑所有浏览器事件
  但如果抽丝剥茧会发现,事件系统的核心只有两个模块:
  · SyntheticEvent(合成事件)
  · 模拟实现的事件传播机制
  本文会用60行代码实现这两个模块,让你快速了解React事件系统的原理。
  在线DEMO地址[1]
  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结构类似如下:
  捕获阶段的实现由于我们是从目标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个步骤实现。
  总的来说,就是这么简单。

TAG: 软件开发

 

评分:0

我来说两句

Open Toolbar