3

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

 2 years ago
source link: https://segmentfault.com/a/1190000041355419
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

大家好,我卡颂。

由于如下原因,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结构类似如下:

捕获阶段的实现

由于我们是从目标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个步骤实现。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK