大家好,我是Kason。由于以下原因,React的事件系统代码量非常大:需要抹平不同浏览器之间的差异,内部优先级机制绑定需要考虑所有浏览器事件。但是,如果你抽丝剥茧,你会发现事件系统的核心只有两个模块:SyntheticEvent(合成事件)模拟实现的事件传播机制本文将用60行代码来实现这两个模块,让让你快速了解React事件系统的原理。在线DEMO地址欢迎加入人类优质前端框架群,飞天demo效果针对如下JSX:constjsx=(console.log("clicksection")}>你好
{//e.stopPropagation();console.log("点击按钮");}}>点击节>);在浏览器中渲染:constroot=document.querySelector("#root");ReactDOM.render(jsx,root);点击按钮,会依次打印:clickbuttonclicksection如果加上e.stopPropagation(),点击后会打印:clickbutton我们的目标是用ONCLICK替换JSX中的onClick,但是点击后的效果不变。也就是说,我们将基于React自制一个事件系统,其事件名称的书写规则为ONXXX形式的全部大写。实施SyntheticEvent首先,让我们实施SyntheticEvent(合成事件)。SyntheticEvent是对浏览器原生事件对象的一层封装。与所有浏览器兼容,并具有与浏览器原生事件相同的API,例如stopPropagation()和preventDefault()。SyntheticEvent的目的是抹平浏览器在事件对象上的差异,但是对于不支持某个事件的浏览器,SyntheticEvent不提供polyfill(因为它会显着增加ReactDOM的大小)。我们的实现很简单:classSyntheticEvent{constructor(e){this.nativeEvent=e;}stopPropagation(){this._stopPropagation=true;如果(this.nativeEvent.stopPropagation){this.nativeEvent.stopPropagation();}}}接收本机事件对象并返回包装器对象。本机事件对象将存储在nativeEvent属性中。同时实现stopPropagation方法。实际的SyntheticEvent将包含更多的属性和方法。此处,出于演示目的简化了事件传播机制。事件传播机制的实现步骤如下:在根节点绑定事件类型对应的事件回调,所有触发该类型事件的后代节点最终都会将事件回调处理委托给根节点。找到触发事件的DOM节点,找到其对应的FiberNode(即虚拟DOM节点)并收集从当前FiberNode到根FiberNode所有事件对应的注册回调,反向遍历并执行所有收集到的回调(模拟捕获阶段的实现)正向遍历并执行所有收集到的回调(模拟冒泡阶段的实现)首先实现第一步://Step1constaddEvent=(container,type)=>{container.addEventListener(type,(e)=>{//dispatchEvent是需要实现的“根节点事件回调”dispatchEvent(e,type.toUpperCase(),container);});};在入口处注册点击回调:constroot=document.querySelector("#root");ReactDOM.render(jsx,root);//添加如下代码addEvent(root,"click");接下来实现根节点的事件回调:constdispatchEvent=(e,type)=>{//封装合成事件constse=newSyntheticEvent(e);constele=e.target;//对比hack方法,通过DOM节点找到对应的FiberNodeletfiber;for(letpropinele){if(prop.toLowerCase().includes("fiber")){fiber=ele[prop];}}//第三步:收集路径中的“本次事件的所有回调函数”constpaths=collectPaths(type,fiber);//第4步:捕获阶段的实现triggerEventFlow(paths,type+"CAPTURE",se);//第五步:冒泡阶段的实现if(!se._stopPropagation){triggerEventFlow(paths.reverse(),type,se);}};下一个集合路径中事件的所有回调函数集合路径中事件回调函数的思路是从当前FiberNode向上遍历到根FiberNode。收集遍历过程中保存在FiberNode.memoizedProps属性中的对应事件回调:constcollectPaths=(type,begin)=>{constpaths=[];//如果不是根FiberNode,会一直向上遍历while(begin.tag!==3){const{memoizedProps,tag}=begin;//5表示DOM节点对应于FiberNodeif(tag===5){consteventName=("on"+type).toUpperCase();//如果包含相应的事件回调,保存在路径中if(memoizedProps&&Object.keys(memoizedProps).includes(eventName)){constpathNode={};pathNode[type.toUpperCase()]=memoizedProps[eventName];paths.push(路径节点);}}begin=begin.return;}返回路径;};得到的paths结构类似如下:捕获阶段的实现由于我们是从目标FiberNode向上遍历,收集到的回调顺序是:[目标事件回调,一些祖先事件回调,一些更远的祖先回调...]模拟捕获阶段的实现,需要从后向前遍历数组,执行回调。遍历的方法如下:consttriggerEventFlow=(paths,type,se)=>{//从后向前遍历for(leti=paths.length;i--;){constpathNode=paths[i];constcallback=pathNode[类型];if(callback){//有回调函数,传入合成事件,执行callback.call(null,se);}if(se._stopPropagation){//如果执行了se.stopPropagation(),则取消下一次遍历中断;}}};注意我们在SyntheticEvent中实现的stopPropagation方法调用后会阻止继续遍历。冒泡阶段的实现有了捕获阶段实现的经验,冒泡阶段实现起来很简单,把路径倒转一遍遍历即可。总结React事件系统的核心由两部分组成:SyntheticEvent事件传播机制事件传播机制分5步实现。总的来说,就是这么简单。