React事件机制-事件分发
事件分发
之前讲述了事件如何绑定在document
上,那么具体事件触发的时候是如何分发到具体的监听者呢?我们接着上次注册的事件代理看。当我点击update counter
按钮时,触发注册的click
事件代理。
function dispatchinteractiveevent(topleveltype, nativeevent) { interactiveupdates(dispatchevent, topleveltype, nativeevent); } function interactiveupdates(fn, a, b) { return _interactiveupdatesimpl(fn, a, b); } var _interactiveupdatesimpl = function (fn, a, b) { return fn(a, b); };
topleveltype
为click
,nativeevent
为真实dom事件对象。看似很多,其实就做了一件事: 执行dispatchevent(topleveltype, nativeevent)
。其实不然,_interactiveupdatesimpl
在后面被重新赋值为interactiveupdates$1
,完成了一次自我蜕变。
function setbatchingimplementation(batchedupdatesimpl, interactiveupdatesimpl, flushinteractiveupdatesimpl) { _batchedupdatesimpl = batchedupdatesimpl; _interactiveupdatesimpl = interactiveupdatesimpl; _flushinteractiveupdatesimpl = flushinteractiveupdatesimpl; } function interactiveupdates$1(fn, a, b) { if (!isbatchingupdates && !isrendering && lowestprioritypendinginteractiveexpirationtime !== nowork) { performwork(lowestprioritypendinginteractiveexpirationtime, false); lowestprioritypendinginteractiveexpirationtime = nowork; } var previousisbatchingupdates = isbatchingupdates; isbatchingupdates = true; try { return scheduler.unstable_runwithpriority(scheduler.unstable_userblockingpriority, function () { return fn(a, b); }); } finally { isbatchingupdates = previousisbatchingupdates; if (!isbatchingupdates && !isrendering) { performsyncwork(); } } } setbatchingimplementation(batchedupdates$1, interactiveupdates$1, flushinteractiveupdates$1);
如果有任何等待的交互更新,条件满足的情况下会先同步更新,然后设置isbatchingupdates
,进行scheduler
调度。最后同步更新。scheduler
的各类优先级如下:
unstable_immediatepriority: 1 unstable_userblockingpriority: 2 unstable_normalpriority: 3 unstable_lowpriority: 4 unstable_idlepriority: 5
进入scheduler
调度,根据优先级计算时间,开始执行传入的回调函数。然后调用dispatchevent
,最后更新immediate work
。flushimmediatework
里的调用关系很复杂,最终会调用requestanimationframe
进行更新,这里不进行过多讨论。
function unstable_runwithpriority(prioritylevel, eventhandler) { switch (prioritylevel) { case immediatepriority: case userblockingpriority: case normalpriority: case lowpriority: case idlepriority: break; default: prioritylevel = normalpriority; } var previousprioritylevel = currentprioritylevel; var previouseventstarttime = currenteventstarttime; currentprioritylevel = prioritylevel; currenteventstarttime = exports.unstable_now(); try { return eventhandler(); } finally { currentprioritylevel = previousprioritylevel; currenteventstarttime = previouseventstarttime; flushimmediatework(); } }
下面看看dispatchevent
的具体执行过程。
function dispatchevent(topleveltype, nativeevent) { if (!_enabled) { return; } // 获取事件触发的原始节点 var nativeeventtarget = geteventtarget(nativeevent); // 获取原始节点最近的fiber对象(通过缓存在dom上的internalinstancekey属性来寻找),如果没找到会往父节点继续寻找。 var targetinst = getclosestinstancefromnode(nativeeventtarget); if (targetinst !== null && typeof targetinst.tag === 'number' && !isfibermounted(targetinst)) { targetinst = null; } // 创建对象,包含事件名称,原始事件,目标fiber对象和ancestor(空数组);如果缓存池有则直接取出并根据参数初始化属性。 var bookkeeping = gettoplevelcallbackbookkeeping(topleveltype, nativeevent, targetinst); try { // 批处理事件 batchedupdates(handletoplevel, bookkeeping); } finally { // 释放bookkeeping对象内存,并放入对象池缓存 releasetoplevelcallbackbookkeeping(bookkeeping); } }
接着看batchedupdates
,其实就是设置isbatching
变量然后调用handletoplevel(bookkeeping)
。
function batchedupdates(fn, bookkeeping) { if (isbatching) { return fn(bookkeeping); } isbatching = true; try { // _batchedupdatesimpl其实指向batchedupdates$1函数,具体细节这里不再赘述 return _batchedupdatesimpl(fn, bookkeeping); } finally { isbatching = false; var controlledcomponentshavependingupdates = needsstaterestore(); if (controlledcomponentshavependingupdates) { _flushinteractiveupdatesimpl(); restorestateifneeded(); } } }
所以将原始节点对应最近的fiber
缓存在bookkeeping.ancestors
中。
function handletoplevel(bookkeeping) { var targetinst = bookkeeping.targetinst; var ancestor = targetinst; do { if (!ancestor) { bookkeeping.ancestors.push(ancestor); break; } var root = findrootcontainernode(ancestor); if (!root) { break; } bookkeeping.ancestors.push(ancestor); ancestor = getclosestinstancefromnode(root); } while (ancestor); for (var i = 0; i < bookkeeping.ancestors.length; i++) { targetinst = bookkeeping.ancestors[i]; runextractedeventsinbatch(bookkeeping.topleveltype, targetinst, bookkeeping.nativeevent, geteventtarget(bookkeeping.nativeevent)); } }
runextractedeventsinbatch
中调用了两个方法: extractevents
和runeventsinbatch
。前者构造合成事件,后者批处理合成事件。
function runextractedeventsinbatch(topleveltype, targetinst, nativeevent, nativeeventtarget) { var events = extractevents(topleveltype, targetinst, nativeevent, nativeeventtarget); runeventsinbatch(events); }
事件合成
function extractevents(topleveltype, targetinst, nativeevent, nativeeventtarget) { var events = null; for (var i = 0; i < plugins.length; i++) { var possibleplugin = plugins[i]; if (possibleplugin) { var extractedevents = possibleplugin.extractevents(topleveltype, targetinst, nativeevent, nativeeventtarget); if (extractedevents) { events = accumulateinto(events, extractedevents); } } } return events; }
plugins是所有合成事件集合的数组,eventpluginhub
初始化的时候完成注入。遍历所有plugins
,调用其extractevents
方法,返回构造的合成事件。accumulateinto
函数则把合成事件放入events
。本例click
事件合适的plugin
是simpleeventplugin
,其他plugin得到的extractedevents
都不满足if (extractedevents)
条件。
eventpluginhubinjection.injecteventpluginsbyname({ simpleeventplugin: simpleeventplugin, enterleaveeventplugin: enterleaveeventplugin, changeeventplugin: changeeventplugin, selecteventplugin: selecteventplugin, beforeinputeventplugin: beforeinputeventplugin, });
接下来看看构造合成事件的具体过程,这里针对simpleeventplugin
,其他plugin
就不一一分析了,来看下其extractevents
:
extractevents: function(topleveltype, targetinst, nativeevent, nativeeventtarget) { var dispatchconfig = topleveleventstodispatchconfig[topleveltype]; if (!dispatchconfig) { return null; } var eventconstructor = void 0; switch (topleveltype) { ... case top_click: ... eventconstructor = syntheticmouseevent; break; ... } var event = eventconstructor.getpooled(dispatchconfig, targetinst, nativeevent, nativeeventtarget); accumulatetwophasedispatches(event); return event; }
topleveleventstodispatchconfig
是一个map对象,存储着各类事件对应的配置信息。这里获取到click
的配置信息,然后根据topleveltype
选择对应的合成构造函数,这里为syntheticmouseevent
。接着从syntheticmouseevent
合成事件对象池中获取合成事件。调用eventconstructor.getpooled
,最终调用的是getpooledevent
。
注意: syntheticevent.extend方法中明确写有addeventpoolingto(class);所以,syntheticmouseevent有eventpool、getpooled和release属性。后面会详细介绍syntheticevent.extend
function addeventpoolingto(eventconstructor) { eventconstructor.eventpool = []; eventconstructor.getpooled = getpooledevent; eventconstructor.release = releasepooledevent; }
首次触发事件,对象池为空,所以这里需要新创建。如果不为空,则取出一个并初始化。
function getpooledevent(dispatchconfig, targetinst, nativeevent, nativeinst) { var eventconstructor = this; if (eventconstructor.eventpool.length) { var instance = eventconstructor.eventpool.pop(); eventconstructor.call(instance, dispatchconfig, targetinst, nativeevent, nativeinst); return instance; } return new eventconstructor(dispatchconfig, targetinst, nativeevent, nativeinst); }
合成事件的属性是由react
主动生成的,一些属性和原生事件的属性名完全一致,使其完全符合w3c标准,因此在事件层面上具有跨浏览器兼容性。如果要访问原生对象,通过nativeevent
属性即可获取。这里syntheticmouseevent
由syntheticuievent
扩展而来,而syntheticuievent
由syntheticevent
扩展而来。
var syntheticmouseevent = syntheticuievent.extend({ ... }); var syntheticuievent = syntheticevent.extend({ ... }); syntheticevent.extend = function (interface) { var super = this; // 原型继承 var e = function () {}; e.prototype = super.prototype; var prototype = new e(); // 构造继承 function class() { return super.apply(this, arguments); } _assign(prototype, class.prototype); class.prototype = prototype; class.prototype.constructor = class; class.interface = _assign({}, super.interface, interface); class.extend = super.extend; addeventpoolingto(class); return class; };
当被new创建时,会调用父类syntheticevent
进行构造。主要是将原生事件上的属性挂载到合成事件上,还配置了一些额外属性。
function syntheticevent(dispatchconfig, targetinst, nativeevent, nativeeventtarget) { this.dispatchconfig = dispatchconfig; this._targetinst = targetinst; this.nativeevent = nativeevent; ... }
合成事件构造完成后,调用accumulatetwophasedispatches
。
function accumulatetwophasedispatches(events) { foreachaccumulated(events, accumulatetwophasedispatchessingle); } // 循环处理所有的合成事件 function foreachaccumulated(arr, cb, scope) { if (array.isarray(arr)) { arr.foreach(cb, scope); } else if (arr) { cb.call(scope, arr); } } // 检测事件是否具有捕获阶段和冒泡阶段 function accumulatetwophasedispatchessingle(event) { if (event && event.dispatchconfig.phasedregistrationnames) { traversetwophase(event._targetinst, accumulatedirectionaldispatches, event); } } function traversetwophase(inst, fn, arg) { var path = []; // 循环遍历当前元素及父元素,缓存至path while (inst) { path.push(inst); inst = getparent(inst); } var i = void 0; // 捕获阶段 for (i = path.length; i-- > 0;) { fn(path[i], 'captured', arg); } // 冒泡阶段 for (i = 0; i < path.length; i++) { fn(path[i], 'bubbled', arg); } } function accumulatedirectionaldispatches(inst, phase, event) { // 获取当前阶段对应的事件处理函数 var listener = listeneratphase(inst, event, phase); // 将相关listener和目标fiber挂载到event对应的属性上 if (listener) { event._dispatchlisteners = accumulateinto(event._dispatchlisteners, listener); event._dispatchinstances = accumulateinto(event._dispatchinstances, inst); } }
事件执行(批处理合成事件)
首先将events
合并到事件队列,之前没有处理完毕的队列也一同合并。如果新的事件队列为空,则退出。反之开始循环处理事件队列中每一个event
。foreachaccumulated
前面有提到过,这里不再赘述。
function runeventsinbatch(events) { if (events !== null) { eventqueue = accumulateinto(eventqueue, events); } var processingeventqueue = eventqueue; eventqueue = null; if (!processingeventqueue) { return; } foreachaccumulated(processingeventqueue, executedispatchesandreleasetoplevel); rethrowcaughterror(); }
接下来看看事件处理,executedispatchesandrelease
方法将事件执行和事件清理分开。
var executedispatchesandreleasetoplevel = function (e) { return executedispatchesandrelease(e); }; var executedispatchesandrelease = function (event) { if (event) { // 执行事件 executedispatchesinorder(event); if (!event.ispersistent()) { // 事件清理,将合成事件放入对象池 event.constructor.release(event); } } };
提取事件的处理函数和对应的fiber,调用executedispatch
。
function executedispatchesinorder(event) { var dispatchlisteners = event._dispatchlisteners; var dispatchinstances = event._dispatchinstances; if (array.isarray(dispatchlisteners)) { for (var i = 0; i < dispatchlisteners.length; i++) { if (event.ispropagationstopped()) { break; } executedispatch(event, dispatchlisteners[i], dispatchinstances[i]); } } else if (dispatchlisteners) { executedispatch(event, dispatchlisteners, dispatchinstances); } event._dispatchlisteners = null; event._dispatchinstances = null; }
获取真实dom挂载到event
对象上,然后开始执行事件。
function executedispatch(event, listener, inst) { var type = event.type || 'unknown-event'; // 获取真实dom event.currenttarget = getnodefrominstance(inst); invokeguardedcallbackandcatchfirsterror(type, listener, undefined, event); event.currenttarget = null; }
invokeguardedcallbackandcatchfirsterror
下面调用的方法很多,最终会来到invokeguardedcallbackimpl
,关键就在func.apply(context, funcargs)
;这里的func
就是listener
(本例中是handleclick
),而funcargs
就是合成事件对象。至此,事件执行完毕。
var invokeguardedcallbackimpl = function (name, func, context, a, b, c, d, e, f) { var funcargs = array.prototype.slice.call(arguments, 3); try { func.apply(context, funcargs); } catch (error) { this.onerror(error); } };
事件清理
事件执行完之后,剩下就是一些清理操作。event.constructor.release(event)
相当于releasepooledevent(event)
。由于click
对应的是syntheticmouseevent
,所以会放入syntheticmouseevent.eventpool
中。event_pool_size
固定为10。
function releasepooledevent(event) { var eventconstructor = this; event.destructor(); if (eventconstructor.eventpool.length < event_pool_size) { eventconstructor.eventpool.push(event); } }
这里做了两件事,第一手动释放event
属性上的内存(将属性置为null
),第二将event
放入对象池。至此,清理工作完毕。
destructor: function () { ... this.dispatchconfig = null; this._targetinst = null; this.nativeevent = null; this.isdefaultprevented = functionthatreturnsfalse; this.ispropagationstopped = functionthatreturnsfalse; this._dispatchlisteners = null; this._dispatchinstances = null; ... }
event
清理完后,还会清理bookkeeping
,同样也会放入对象池进行缓存。同样callback_bookkeeping_pool_size
也固定为10。
// callbackbookkeepingpool是react-dom中的全局变量 function releasetoplevelcallbackbookkeeping(instance) { instance.topleveltype = null; instance.nativeevent = null; instance.targetinst = null; instance.ancestors.length = 0; if (callbackbookkeepingpool.length < callback_bookkeeping_pool_size) { callbackbookkeepingpool.push(instance); } }
总结
最后执行performsyncwork
。如果执行的事件内调用了this.setstate
,会进行reconciliation
和commit
。由于事件流的执行是批处理过程,同步调用this.setstate
不会立马更新,需等待所有事件执行完成,即scheduler
调度完后才开始performsyncwork
,最终才能拿到新的state
。如果是settimeout
或者是在dom上另外addeventlistener
的回调函数中调用this.setstate
则会立马更新。因为执行回调函数的时候不经过react
事件流。