乐趣区

关于javascript:60行代码实现React的事件系统

大家好,我卡颂。

因为如下起因,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 个步骤实现。

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

退出移动版