浅谈React Event实现原理
react 元素的事件处理和 dom元素的很相似。但是有一点语法上的不同:
- react事件绑定属性的命名采用驼峰式写法,而不是小写。
- 如果采用 jsx 的语法你需要传入一个函数作为事件处理函数,而不是一个字符串(dom元素的写法)
并且 react 自己内部实现了一个合成事件,使用 react 的时候通常你不需要使用 addeventlistener 为一个已创建的 dom 元素添加监听器。你仅仅需要在这个元素初始渲染的时候提供一个监听器。
我们看一下这是怎么实现的
react 事件机制分为 事件注册,和事件分发,两个部分
事件注册
// 事件绑定 function handleclick(e) { e.preventdefault(); console.log('the link was clicked.'); } return ( <a href="#" rel="external nofollow" onclick={handleclick}> click me </a> );
上述代码中, onclick 作为一个 props 传入了一个 handleclick,在组件更新和挂载的时候,会对props处理, 事件绑定流程如下:
核心代码:
在 reactdomcomponent.js 进行组件加载 (mountcomponent)、更新 (updatecomponent) 的时候,调用 _updatedomproperties
方法对 props 进行处理:
reactdomcomponent.js
_updatedomproperties: function(lastprops, nextprops, transaction) { ... if (registrationnamemodules.hasownproperty(propkey)) { if (nextprop) { // 如果传入的是事件,去注册事件 enqueueputlistener(this, propkey, nextprop, transaction); } else if (lastprop) { deletelistener(this, propkey); } } ... } // 注册事件 function enqueueputlistener(inst, registrationname, listener, transaction) { var containerinfo = inst._nativecontainerinfo; var doc = containerinfo._ownerdocument; ... // 去doc上注册 listento(registrationname, doc); // 事务结束之后 putlistener transaction.getreactmountready().enqueue(putlistener, { inst: inst, registrationname: registrationname, listener: listener, }); }
看下绑定方法
reactbrowsereventemitter.js
listento
//registrationname:需要绑定的事件 //当前component所属的document,即事件需要绑定的位置 listento: function (registrationname, contentdocumenthandle) { var mountat = contentdocumenthandle; //获取当前document上已经绑定的事件 var islistening = getlisteningfordocument(mountat); ... if (...) { //冒泡处理 reactbrowsereventemitter.reacteventlistener.trapbubbledevent(...); } else if (...) { //捕捉处理 reactbrowsereventemitter.reacteventlistener.trapcapturedevent(...); } ... },
走到最后其实就是 doc.addeventlister(event, callback, false);
可以看出所有事件绑定在document上
所以事件触发的都是reacteventlistener的dispatchevent方法
回调事件储存
listenerbank
react 维护了一个 listenerbank 的变量保存了所有的绑定事件的回调。
回到之前注册事件的方法
function enqueueputlistener(inst, registrationname, listener, transaction) { var containerinfo = inst._nativecontainerinfo; var doc = containerinfo._ownerdocument; if (!doc) { // server rendering. return; } listento(registrationname, doc); transaction.getreactmountready().enqueue(putlistener, { inst: inst, registrationname: registrationname, listener: listener, }); }
当绑定完成以后会执行putlistener。
var listenerbank = {}; var getdictionarykey = function (inst) { //inst为组建的实例化对象 //_rootnodeid为组件的唯一标识 return '.' + inst._rootnodeid; } var eventpluginhub = { //inst为组建的实例化对象 //registrationname为事件名称 //listner为我们写的回调函数,也就是列子中的this.autofocus putlistener: function (inst, registrationname, listener) { ... var key = getdictionarykey(inst); var bankforregistrationname = listenerbank[registrationname] || (listenerbank[registrationname] = {}); bankforregistrationname[key] = listener; ... } }
eventpluginhub在每个react中只实例化一次。也就是说,项目组所有事件的回调都会储存在唯一的listenerbank中。
事件触发
注册事件流程图所示,所有的事件都是绑定在document上。回调统一是reacteventlistener的dispatch方法。
由于冒泡机制,无论我们点击哪个dom,最后都是由document响应(因为其他dom根本没有事件监听)。也即是说都会触发 reacteventlistener.js 里的 dispatch
方法。
我们先看一下事件触发的流程图:
dispatchevent: function (topleveltype, nativeevent) { if (!reacteventlistener._enabled) { return; } // 这里得到toplevelcallbackbookkeeping的实例对象,本例中第一次触发dispatchevent时 // bookkeeping instanceof toplevelcallbackbookkeeping // bookkeeping = toplevelcallbackbookkeeping {topleveltype: "topclick", nativeevent: "click", ancestors: array(0)} var bookkeeping = toplevelcallbackbookkeeping.getpooled(topleveltype, nativeevent); try { // event queue being processed in the same cycle allows // `preventdefault`. // 接着执行handletoplevelimpl(bookkeeping) reactupdates.batchedupdates(handletoplevelimpl, bookkeeping); } finally { // 回收 toplevelcallbackbookkeeping.release(bookkeeping); } } function handletoplevelimpl(bookkeeping) { var nativeeventtarget = geteventtarget(bookkeeping.nativeevent); // 获取当前事件的虚拟dom元素 var targetinst = reactdomcomponenttree.getclosestinstancefromnode(nativeeventtarget); var ancestor = targetinst; do { bookkeeping.ancestors.push(ancestor); ancestor = ancestor && findparent(ancestor); } while (ancestor); for (var i = 0; i < bookkeeping.ancestors.length; i++) { targetinst = bookkeeping.ancestors[i]; // 这里的_handletoplevel 对应的就是reacteventemittermixin.js里的handletoplevel reacteventlistener._handletoplevel(bookkeeping.topleveltype, targetinst, bookkeeping.nativeevent, geteventtarget(bookkeeping.nativeevent)); } } // 这里的findparent曾经给我带来误导,我以为去找当前元素所有的父节点,但其实不是的, // 我们知道一般情况下,我们的组件最后会被包裹在<div id='root'></div>的标签里 // 一般是没有组件再去嵌套它的,所以通常返回null /** * find the deepest react component completely containing the root of the * passed-in instance (for use when entire react trees are nested within each * other). if react trees are not nested, returns null. */ function findparent(inst) { while (inst._hostparent) { inst = inst._hostparent; } var rootnode = reactdomcomponenttree.getnodefrominstance(inst); var container = rootnode.parentnode; return reactdomcomponenttree.getclosestinstancefromnode(container); }
我们看一下核心方法 _handletoplevel
reacteventemittermixin.js
//这就是核心的处理了 handletoplevel: function (topleveltype, targetinst, nativeevent, nativeeventtarget) { //返回合成事件 //这里进入了eventpluginhub,调用事件插件方法,返回合成事件,并执行队列里的dispatchlistener var events = eventpluginhub.extractevents(topleveltype, targetinst, nativeevent, nativeeventtarget); //执行合成事件 runeventqueueinbatch(events); }
合成事件如何生成,请看上方事件触发的流程图
runeventqueuelnbatch(events)做了两件事
- 把 dispatchlistener里面的事件排队push进 eventqueue
- 执行 eventpluginhub.processeventqueue(false);
执行的细节如下:
eventpluginhub.js
// 循环 eventqueue调用 var executedispatchesandreleasetoplevel = function (e) { return executedispatchesandrelease(e, false); }; /* 从event._dispatchlistener 取出 dispatchlistener,然后dispatch事件, * 循环_dispatchlisteners,调用executedispatch */ var executedispatchesandrelease = function (event, simulated) { if (event) { // 在这里dispatch事件 eventpluginutils.executedispatchesinorder(event, simulated); // 释放事件 if (!event.ispersistent()) { event.constructor.release(event); } } }; enqueueevents: function (events) { if (events) { eventqueue = accumulateinto(eventqueue, events); } }, /** * dispatches all synthetic events on the event queue. * * @internal */ processeventqueue: function (simulated) { // set `eventqueue` to null before processing it so that we can tell if more // events get enqueued while processing. var processingeventqueue = eventqueue; eventqueue = null; if (simulated) { foreachaccumulated(processingeventqueue, executedispatchesandreleasesimulated); } else { foreachaccumulated(processingeventqueue, executedispatchesandreleasetoplevel); } // this would be a good time to rethrow if any of the event fexers threw. reacterrorutils.rethrowcaughterror(); }, /** * standard/simple iteration through an event's collected dispatches. */ function executedispatchesinorder(event, simulated) { var dispatchlisteners = event._dispatchlisteners; var dispatchinstances = event._dispatchinstances; if (array.isarray(dispatchlisteners)) { for (var i = 0; i < dispatchlisteners.length; i++) { // 由这里可以看出,合成事件的stoppropagation只能阻止react合成事件的冒泡, // 因为event._dispatchlisteners 只记录了由jsx绑定的绑定的事件,对于原生绑定的是没有记录的 if (event.ispropagationstopped()) { break; } // listeners and instances are two parallel arrays that are always in sync. executedispatch(event, simulated, dispatchlisteners[i], dispatchinstances[i]); } } else if (dispatchlisteners) { executedispatch(event, simulated, dispatchlisteners, dispatchinstances); } event._dispatchlisteners = null; event._dispatchinstances = null; } function executedispatch(event, simulated, listener, inst) { var type = event.type || 'unknown-event'; // 注意这里将事件对应的dom元素绑定到了currenttarget上 event.currenttarget = eventpluginutils.getnodefrominstance(inst); if (simulated) { reacterrorutils.invokeguardedcallbackwithcatch(type, listener, event); } else { // 一般都是非模拟的情况,执行invokeguardedcallback reacterrorutils.invokeguardedcallback(type, listener, event); } event.currenttarget = null; }
由上面的函数可知,dispatch 合成事件分为两个步骤:
- 通过_dispatchlisteners里得到所有绑定的回调函数,在通过_dispatchinstances的绑定回调函数的虚拟dom元素
- 循环执行_dispatchlisteners里所有的回调函数,这里有一个特殊情况,也是react阻止冒泡的原理
其实在 eventpluginhub.js 里主要做了两件事情.
1.从event._dispatchlistener 取出 dispatchlistener,然后dispatch事件,
循环_dispatchlisteners,调用executedispatch,然后走到reacterrorutils.invokeguardedcallback;
2.释放 event
上面这个函数最重要的功能就是将事件对应的dom元素绑定到了currenttarget上,
这样我们通过e.currenttarget就可以找到绑定事件的原生dom元素。
下面就是整个执行过程的尾声了:
reacterrorutils.js
var fakenode = document.createelement('react'); reacterrorutils.invokeguardedcallback = function(name, func, a, b) { var boundfunc = func.bind(null, a, b); var evttype = `react-${name}`; fakenode.addeventlistener(evttype, boundfunc, false); var evt = document.createevent('event'); evt.initevent(evttype, false, false); fakenode.dispatchevent(evt); fakenode.removeeventlistener(evttype, boundfunc, false); };
由invokeguardedcallback可知,最后react调用了faked元素的dispatchevent方法来触发事件,并且触发完毕之后立即移除监听事件。
总的来说,整个click事件被分发的过程就是:
1、用eventpluginhub生成合成事件,这里注意同一事件类型只会生成一个合成事件,里面的_dispatchlisteners里储存了同一事件类型的所有回调函数
2、按顺序去执行它
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。