上篇文檔 React事件機制 - 源碼概覽(上)說到了事件執行階段的構造合成事件部分,本文接着繼續往下分析
批處理合成事件
入口是
runEventsInBatch
// runEventsInBatch
// packages/events/EventPluginHub.js
export function runEventsInBatch(
events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
simulated: boolean,
) {
if (events !== null) {
eventQueue = accumulateInto(eventQueue, events);
}
const processingEventQueue = eventQueue;
eventQueue = null;
if (!processingEventQueue) {
return;
}
if (simulated) {
// react-test 才會執行的代碼
// ...
} else {
forEachAccumulated(
processingEventQueue,
executeDispatchesAndReleaseTopLevel,
);
}
// This would be a good time to rethrow if any of the event handlers threw.
rethrowCaughtError();
}
複制代碼
這個方法首先會将目前需要處理的
events
事件,與之前沒有處理完畢的隊列調用
accumulateInto
方法按照順序進行合并,組合成一個新的隊列,因為之前可能就存在還沒處理完的合成事件,這裡就又有得到執行的機會了
如果合并後的隊列為
null
,即沒有需要處理的事件,則退出,否則根據
simulated
來進行分支判斷調用對應的方法,這裡的
simulated
标志位,字面意思是
仿造的、假裝的
,其實這個字段跟
react-test
,即測試用例有關,隻有測試用例調用
runEventsInBatch
方法的時候,
simulated
标志位的值才為
true
,除了這個地方以外,
React
源碼中還有其他的很多地方都會出現
simulated
,都是跟測試用例有關,看到了不用管直接走
else
邏輯即可,是以我們這裡就走
else
的邏輯,調用
forEachAccumulated
方法
// packages/events/forEachAccumulated.js
function forEachAccumulated<T>(
arr: ?(Array<T> | T),
cb: (elem: T) => void,
scope: ?any,
) {
if (Array.isArray(arr)) {
arr.forEach(cb, scope);
} else if (arr) {
cb.call(scope, arr);
}
}
複制代碼
這個方法就是先看下事件隊列
processingEventQueue
是不是個數組,如果是數組,說明隊列中不止一個事件,則周遊隊列,調用
executeDispatchesAndReleaseTopLevel
,否則說明隊列中隻有一個事件,則無需周遊直接調用即可
是以來看下
executeDispatchesAndReleaseTopLevel
這個方法:
// packages/events/EventPluginHub.js
const executeDispatchesAndReleaseTopLevel = function(e) {
return executeDispatchesAndRelease(e, false);
};
// ...
const executeDispatchesAndRelease = function(
event: ReactSyntheticEvent,
simulated: boolean,
) {
if (event) {
executeDispatchesInOrder(event, simulated);
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
複制代碼
executeDispatchesAndReleaseTopLevel
又調用了
executeDispatchesAndRelease
,然後
executeDispatchesAndRelease
這個方法先調用了
executeDispatchesInOrder
,這個方法是事件處理的核心所在:
// packages/events/EventPluginUtils.js
// executeDispatchesInOrder
export function executeDispatchesInOrder(event, simulated) {
const dispatchListeners = event._dispatchListeners;
const dispatchInstances = event._dispatchInstances;
if (__DEV__) {
validateEventDispatches(event);
}
if (Array.isArray(dispatchListeners)) {
for (let i = 0; i < dispatchListeners.length; i++) {
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;
}
複制代碼
首先對拿到的事件上挂在的
dispatchListeners
,也就是之前拿到的目前元素以及其所有父元素上注冊的事件回調函數的集合,周遊這個集合,如果發現周遊到的事件的
event.isPropagationStopped()
為
true
,則周遊的循環直接
break
掉,這裡的
isPropagationStopped
在前面已經說過了,它是用于辨別目前
React Node
上觸發的事件是否執行了
e.stopPropagation()
這個方法,如果執行了,則說明在此之前觸發的事件已經調用
event.stopPropagation()
,
isPropagationStopped
的值被置為
functionThatReturnsTrue
,即執行後為
true
,目前事件以及後面的事件作為父級事件就不應該再被執行了
這裡當
event.isPropagationStopped()
為
true
時,中斷合成事件的向上周遊執行,也就起到了和原生事件調用
stopPropagation
相同的效果
如果循環沒有被中斷,則繼續執行
executeDispatch
方法,這個方法接下來又一層一層地調了很多方法,最終來到
invokeGuardedCallbackImpl
:
// packages/shared/invokeGuardedCallbackImpl.js
let invokeGuardedCallbackImpl = function<A, B, C, D, E, F, Context>(
name: string | null,
func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed,
context: Context,
a: A,
b: B,
c: C,
d: D,
e: E,
f: F,
) {
const funcArgs = Array.prototype.slice.call(arguments, 3);
try {
func.apply(context, funcArgs);
} catch (error) {
this.onError(error);
}
};
複制代碼
關鍵在于這一句:
func.apply(context, funcArgs);
複制代碼
funcArgs
是什麼呢?其實就是合成事件對象,包括原生浏覽器事件對象的基本上所有屬性和方法,除此之外還另外挂載了額外其他一些跟
React
合成事件相關的屬性和方法,而
func
則就是傳入的事件回調函數,對于本示例來說,就等于
clickHandler
這個回調方法:
// func === clickHandler
clickHandler(e) {
console.log('click callback', e)
}
複制代碼
将
funcArgs
作為參數傳入
func
,也即是傳入
clickHandler
,是以我們就能夠在
clickHandler
這個函數體内拿到
e
這個回調參數,也就能通過這個回調參數拿到其上面挂載的任何屬性和方法,例如一些跟原生浏覽器對象相關的屬性和方法,以及原生事件對象本身(
nativeEvent
)
至此,事件執行完畢
這個過程流程圖如下:
<figure>[圖檔上傳中...(image-6009c0-1541854808587-0)]
<figcaption></figcaption>
</figure>
事件清理
事件執行完畢之後,接下來就是一些清理工作了,因為
React
采用了對象池的方式來管理合成事件,是以當事件執行完畢之後就要清理釋放掉,減少記憶體占用,主要是執行了上面提到過的位于
executeDispatchesAndRelease
方法中的
event.constructor.release(event);
這一句代碼
這裡面的
release
就是如下方法:
// packages/events/SyntheticEvent.js
function releasePooledEvent(event) {
const EventConstructor = this;
invariant(
event instanceof EventConstructor,
'Trying to release an event instance into a pool of a different type.',
);
event.destructor();
if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
EventConstructor.eventPool.push(event);
}
}
複制代碼
這個方法主要做了兩件事,首先釋放掉
event
上屬性占用的記憶體,然後把清理後的
event
對象再放入對象池中,可以被後續事件對象二次利用
event.destructor();
這句就是用于釋放記憶體的,
destructor
這個方法的字面意思是
析構
,也就表示它是一個析構函數,了解
C/C++
的人應該對這個名詞很熟悉,它一般都是用于 清理善後的工作,例如釋放掉構造函數申請的記憶體空間以釋放記憶體,這裡的
destructor
方法同樣是有着這個作用
destructor
是
SyntheticEvent
上的方法,是以所有的合成事件都能拿到這個方法:
// packages/events/SyntheticEvent.js
destructor: function() {
const Interface = this.constructor.Interface;
for (const propName in Interface) {
if (__DEV__) {
Object.defineProperty(
this,
propName,
getPooledWarningPropertyDefinition(propName, Interface[propName]),
);
} else {
this[propName] = null;
}
}
this.dispatchConfig = null;
this._targetInst = null;
this.nativeEvent = null;
this.isDefaultPrevented = functionThatReturnsFalse;
this.isPropagationStopped = functionThatReturnsFalse;
this._dispatchListeners = null;
this._dispatchInstances = null;
// 以下省略部分代碼
// ...
}
複制代碼
JavaScript
引擎有自己的垃圾回收機制,一般來說不需要開發者親自去回收記憶體空間,但這并不是說開發者就完全無法影響這個過程了,常見的手動釋放記憶體的方法就是将對象置為
null
,
destructor
這個方法主要就是做這件事情,周遊事件對象上所有屬性,并将所有屬性的值置為
null
總結
React
的事件機制看起來還是比較複雜的,我自己看了幾遍源碼又對着調試了幾遍,現在又寫了分析文章,回頭再想想其實主線還是比較明确的,過完了源碼之後,再去看
react-dom/src/events/ReactBrowserEventEmitter.js
這個源碼檔案開頭的那一段圖形化注釋,整個流程就更加清晰了
順便分享一個看源碼的技巧,如果某份源碼,比如
React
這種,比較複雜,代碼方法很多,很容易看着看着就亂了,那麼就不要再幹看着了,直接寫個簡單的例子,然後在浏覽器上打斷點,對着例子和源碼一步步調試,弄明白每一步的邏輯和目的,多調試幾次後,基本上就能抓到關鍵點了,後續再通讀源碼的時候,就會流暢很多了
作者:清夜
連結:https://juejin.im/post/5bd32b5cf265da0af609f79a
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。