大家好,我是Kason。由于以下原因,React的事件系统代码量非常大:需要磨平不同浏览器之间的差异,内部“优先级机制”绑定需要考虑所有浏览器事件。Module:SyntheticEvent(合成事件)模拟事件传播机制。本文将用60行代码来实现这两个模块,让你快速了解React事件系统的原理。在线DEMO地址[1]Demo的效果针对如下JSX:constjsx=(console.log("clicksection")}>你好
{//e.stopPropagation();console.log("clickbutton");}}>click);在浏览器中呈现: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);//第四步:捕获阶段的实现triggerEventFlow(paths,type+"CAPTURE",se);//第五步:冒泡阶段的实现if(!se._stopPropagation){triggerEventFlow(paths.reverse(),type,se);}};接下来,集合路径中“本次事件的所有回调函数”集合路径中事件回调函数的实现思路为:从当前FiberNode遍历到根FiberNode。collection遍历过程中保存在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步实现。总的来说,就是这么简单。参考[1]在线DEMO地址:https://codesandbox.io/s/optimistic-torvalds-9ufc5?file=/src/index.js