事件分發 之前講述了事件如何綁定在 上,那麼具體事件觸發的時候是如何分發到具體的監聽者呢?我們接著上次註冊的事件代理看。當我點擊 按鈕時,觸發註冊的 事件代理。 為`click nativeEvent dispatchEvent(topLevelType, nativeEvent) _interac ...
事件分發
之前講述了事件如何綁定在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
事件流。