NodeNote,持續更新中react相關庫源碼淺析, react ts3 項目react
<div id="app"></div>
<div onclick="alert(1)">原生</div>
class APP extends React.Component{
render(){
return (
<div>
<Header/>
</div>
)
}
}
class Header extends React.Component{
clickHandler(){
console.log("click")
}
render(){
return (
<div>
<div onClick={this.clickHandler.bind(this)} a={1}>
this is Header
</div>
<p onClick={this.clickHandler.bind(this)} a={1}>
this is Header
</p>
</div>
)
}
}
ReactDOM.render(
<APP/>,
document.getElementById('app')
);
複製代碼
上述的組件中點擊事件的觸發過程以下:git
document
監聽到某個
DOM
上冒泡上來的點擊事件以後,調用
document
上的處理函數
dispatchInteractiveEvent
:
function dispatchInteractiveEvent(topLevelType, nativeEvent) {
interactiveUpdates(dispatchEvent, topLevelType, nativeEvent);
}
複製代碼
interactiveUpdates
函數的做用是執行:dispatchEvent(topLevelType, nativeEvent)
github
dispatchEvent(topLevelType, nativeEvent)
緣由以下:
export function interactiveUpdates(fn, a, b) {
return _interactiveUpdatesImpl(fn, a, b);
}
let _interactiveUpdatesImpl = function(fn, a, b) {
return fn(a, b);
};
複製代碼
dispatchEvent
會調用batchedUpdates
,其中會調用handleTopLevel
,handleTopLevel
會調用runExtractedEventsInBatch
數組
function runExtractedEventsInBatch(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
// 合成事件的生成以及在fiber樹上經過模擬捕獲與冒泡收集事件處理函數與對應節點並存儲到合成事件的相關屬性上
var events = extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
// 開始執行合成事件上的相關屬性存儲的執行事件處理函數
runEventsInBatch(events);
}
複製代碼
runExtractedEventsInBatch
函數中會先將事件名稱topLevelType
、對應的react
元素實例targetInst
、原生事件對象nativeEvent
以及事件做用的DOM
傳入extractEvents
中,extractEvents
函數會遍歷事件插件數組plugins
,並經過傳入的事件名稱topLevelType
選擇對應的plugin
,並調用該plugin
上的extractEvents
,生成合成事件SyntheticEvent
,並收集事件觸發的目標節點以及以上的祖先節點須要觸發的事件處理函數和對於的fiber
分別存入合成事件的_dispatchListeners
與_dispatchInstances
屬性上,捕獲階段的函數與節點在數組靠前位置,祖先節點‘越老’其事件處理函數以及該節點在數組中的位置越靠前;冒泡階段的函數與節點在數組靠後位置,祖先節點‘越老’其事件處理函數以及該節點在數組中的位置越靠後;bash
runExtractedEventsInBatch函數中合成事件對象的邏輯:
var events = ex:tractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
複製代碼
對於點擊事件調用的是simpleeventplugin
上的extractEvents
函數,該函數會傳入的參數是dispatchConfig, targetInst, nativeEvent, nativeEventTarget
,其中dispatchConfig
是由topLevelType
從topLevelEventsToDispatchConfig
數組中獲取的配置,在simpleeventplugin.extractEvents
函數中會調用以下代碼根據事件配置dispatchConfig
將事件對應的react
元素實例、原生事件、原生事件對應的DOM
封裝成爲一個合成事件。數據結構
var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget)
複製代碼
EventConstructor.getPooled
實際就是react\packages\events\SyntheticEvent.js
下的getPooledEvent
函數,從其代碼中能夠看到會從事件池中去取一個合成事件對象,而後利用這個對象用新的 dispatchConfig,targetInst,nativeEvent,nativeInst
從新初始化便可;若是事件池爲空,則新建立一個合成事件對象。閉包
function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
const EventConstructor = this;
if (EventConstructor.eventPool.length) {
const instance = EventConstructor.eventPool.pop();
EventConstructor.call(
instance,
dispatchConfig,
targetInst,
nativeEvent,
nativeInst,
);
return instance;
}
return new EventConstructor(
dispatchConfig,
targetInst,
nativeEvent,
nativeInst,
);
}
複製代碼
function SyntheticEvent(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget,
) {
this.dispatchConfig = dispatchConfig;
this._targetInst = targetInst;
this.nativeEvent = nativeEvent;
const Interface = this.constructor.Interface;
for (const propName in Interface) {
if (!Interface.hasOwnProperty(propName)) {
continue;
}
const normalize = Interface[propName];
if (normalize) {
this[propName] = normalize(nativeEvent);
} else {
if (propName === 'target') {
this.target = nativeEventTarget;
} else {
this[propName] = nativeEvent[propName];
}
}
}
const defaultPrevented =
nativeEvent.defaultPrevented != null
? nativeEvent.defaultPrevented
: nativeEvent.returnValue === false;
if (defaultPrevented) {
this.isDefaultPrevented = functionThatReturnsTrue;
} else {
this.isDefaultPrevented = functionThatReturnsFalse;
}
this.isPropagationStopped = functionThatReturnsFalse;
return this;
}
複製代碼
能夠看到原生事件存儲在合成事件對象的nativeEvent
屬性上,目標react
元素實例存儲在_targetInst
屬性上,dispatchConfig
存儲在dispatchConfig
屬性上,將原生事件對應的DOM
即nativeEventTarget
存儲在合成事件的target屬性上。原生事件對象上type
、eventPhase
、bubbles
、cancelable
、defaultPrevented
、isTrusted
存儲在合成事件相同名稱的屬性上。app
從getPooledEvent
函數與合成事件對象的數據結構可知,React
合成的SyntheticEvent
採用了池的思想,從而達到節約內存,避免頻繁的建立和銷燬事件對象的目的。函數
這個是在特定的事件插件的extractEvents
函數中調用EventConstructor.getPooled
獲取合成事件以後進行處理函數的收集。即以下調用accumulateTwoPhaseDispatches(event)
性能
var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
accumulateTwoPhaseDispatches(event);
複製代碼
accumulateTwoPhaseDispatches
中收集事件處理函數的調用棧爲:
export function traverseTwoPhase(inst, fn, arg) {
const path = [];
//將inst的父節點入棧,數組最後的爲最遠的祖先
while (inst) {
path.push(inst);
inst = getParent(inst);
}
let i;
//從最遠的祖先開始向inst節點捕獲執行fn
for (i = path.length; i-- > 0; ) {
fn(path[i], 'captured', arg);
}
//從inst節點開始向最遠的祖先節點冒泡執行fn
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
複製代碼
在虛擬DOM樹中其實就是fiber
樹,將當前事件觸發的目標節點開始向上遍歷的祖先節點挨個存入path
中,而後從祖先節點開始向目標節點進行遍歷對應的就是從數組的從尾向頭開始遍歷(這裏模擬的是捕獲,因此從祖先節點開始向下遍歷),這個遍歷過程當中,將遍歷到的當前節點、合成事件對象、表明捕獲仍是冒泡階段的標誌做爲參數傳入accumulateDirectionalDispatches
,在其中執行listenerAtPhase
獲取該綁定在該節點上須要在捕獲階段觸發的事件處理函數,而後將獲取到的事件處理函數listener
與事件上的存儲處理函數數組event._dispatchListeners
傳入accumulateInto
,並將當前處理函數push
到event._dispatchListeners
中;一樣調用accumulateInto
將捕獲階段中當前綁定了須要捕獲階段觸發的事件的節點存儲到event._dispatchInstances
,至此accumulateDirectionalDispatches
執行完畢,也就是收集到了全部須要捕獲階段執行的事件處理函數與相對應的節點分別存儲在合成事件對象上的_dispatchListeners
與_dispatchInstances
上。捕獲階段,父節點以及其處理函數位於數組的開頭部分,模擬捕獲的事件觸發順序。
按照上述邏輯,冒泡階段,對應與在traverseTwoPhase
中的第二個for
循環,會依次調用accumulateDirectionalDispatches
對事件觸發的目標節點以及以上的父節點進行事件處理函數與綁定了事件處理函數的節點的收集,並將這些函數與節點分別添加在合成事件的_dispatchListeners
與_dispatchInstances
上。
function accumulateDirectionalDispatches(inst, phase, event) {
{
!inst ? warningWithoutStack$1(false, 'Dispatching inst must not be null') : void 0;
}
var listener = listenerAtPhase(inst, event, phase);
if (listener) {
event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
// 獲取捕獲階段的事件名registrationName
function listenerAtPhase(inst, event, propagationPhase) {
var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
return getListener(inst, registrationName);
}
// 開始根據registrationName在當前節點上的props中獲取對應的事件處理函數
function getListener(inst, registrationName) {
var listener = void 0;
// TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
// live here; needs to be moved to a better place soon
var stateNode = inst.stateNode;
if (!stateNode) {
// Work in progress (ex: onload events in incremental mode).
return null;
}
var props = getFiberCurrentPropsFromNode(stateNode);
if (!props) {
// Work in progress.
return null;
}
listener = props[registrationName];
if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
return null;
}
!(!listener || typeof listener === 'function') ? invariant(false, 'Expected `%s` listener to be a function, instead got a value of `%s` type.', registrationName, typeof listener) : void 0;
return listener;
}
複製代碼
至此對於下面的結構:
<div id='div1' onClick={this.clickHandler1.bind(this)} a={1}>
<div id='div2' onClick={this.clickHandler2.bind(this)} a={1}>
<div id='div3' onClick={this.clickHandler3.bind(this)} a={1}>
this is Header
</div>
</div>
</div>
複製代碼
合成事件上的_dispatchListeners
與_dispatchInstances
上分別爲:
_dispatchListener = [
clickHandler1,
clickHandler2,
clickHandler3
]
_dispatchInstances = [
id爲'div1'的fiberNode,
id爲'div2'的fiberNode,
id爲'div3'的fiberNode
]
複製代碼
runExtractedEventsInBatch 執行合成事件對象上的事件處理函數的邏輯:
runEventsInBatch(events);
複製代碼
觸發過程的函數調用棧以下:
_dispatchListeners
上的事件處理函數。到此本文重點在弄清楚了合成事件對象和原生事件對象的關係,以及如何收集
fiber
樹上的事件處理函數。至於如何執行,是明天研究的內容了。固然仍是接着這個寫吧。 從
runEventsInBatch
開始,最後調用合成事件上的事件處理函數:
//將新的合成事件對象添加到原來的對象隊列中,而後進入下一個處理環節forEachAccumulated
export function runEventsInBatch(events) {
if (events !== null) {
// 將當前生成的合成事件對象或者合成事件對象數組添加到以前的合成事件對象隊列中,構成新的隊列
eventQueue = accumulateInto(eventQueue, events);
}
// 將新的合成事件對象隊列eventQueue做爲正在處理的隊列processingEventQueue,並將前者清空
const processingEventQueue = eventQueue;
eventQueue = null;
if (!processingEventQueue) {return;}
//進入下一步
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
}
複製代碼
runEventsInBatch
將新的合成事件對象合併到原來的對象隊列中,而後進入下一個處理環節forEachAccumulated
,可見react
的每一個函數的縝密,每一步都添加了對應的錯誤處理機制。forEachAccumulated
函數源碼:
function forEachAccumulated(arr,cb,scope,) {
if (Array.isArray(arr)) {
arr.forEach(cb, scope);
} else if (arr) {
cb.call(scope, arr);
}
}
複製代碼
forEachAccumulated
會對傳入的arr
數組的元素挨個執行cb(arr[i])
,這裏的cb
爲上一步中傳入的executeDispatchesAndReleaseTopLevel
,arr``爲processingEventQueue
。arr
若是不是數組直接執行cb(arr)
,所以主要看executeDispatchesAndReleaseTopLevel的邏輯:
const executeDispatchesAndReleaseTopLevel = function(e) {
return executeDispatchesAndRelease(e);
};
const executeDispatchesAndRelease = function(event: ReactSyntheticEvent) {
if (event) {
executeDispatchesInOrder(event);
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
複製代碼
先看第二步:這裏先看if
中的if
,event.isPersistent
返回的始終是false
,所以這裏始終會執行release
重置event
上的屬性值,並添加到事件對象池的空餘位置:
react\packages\events\SyntheticEvent.js
//重置event上的屬性值,並添加到事件對象池的空餘位置
function releasePooledEvent(event) {
const EventConstructor = this;
//
event.destructor();
if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
EventConstructor.eventPool.push(event);
}
}
//合成事件對象實例重置
destructor: function() {
const Interface = this.constructor.Interface;
for (const propName in Interface) {
this[propName] = null;
}
this.dispatchConfig = null;
this._targetInst = null;
this.nativeEvent = null;
this.isDefaultPrevented = functionThatReturnsFalse;
this.isPropagationStopped = functionThatReturnsFalse;
this._dispatchListeners = null;
this._dispatchInstances = null;
});
複製代碼
接着看關鍵的第一步,executeDispatchesInOrder
是如何執行合成事件對象上的事件處理函數的:
export function executeDispatchesInOrder(event) {
const dispatchListeners = event._dispatchListeners;
const dispatchInstances = event._dispatchInstances;
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, dispatchListeners[i], dispatchInstances[i]);
}
} else if (dispatchListeners) {
executeDispatch(event, dispatchListeners, dispatchInstances);
}
event._dispatchListeners = null;
event._dispatchInstances = null;
}
複製代碼
從上面代碼的循環能夠看到,遍歷event._dispatchListeners
上的監聽器,首先經過合成事件對象上的isPropagationStopped()
來判斷是否阻止捕獲和冒泡階段中當前事件的進一步傳播,若是有則日後的事件處理函數都沒法被執行。若是沒有阻止傳播,那麼會調用 executeDispatch
執行事件處理函數,最終將合成事件對象上的_dispatchListeners
與_dispatchInstances
清空。如今來看executeDispatch
:
function executeDispatch(event, listener, inst) {
const type = event.type || 'unknown-event';
//獲取當前fiber對應的真實DOM
event.currentTarget = getNodeFromInstance(inst);
//進入下一步
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
event.currentTarget = null;
}
複製代碼
executeDispatch
會將當前傳入的這個fiber
的真實DOM存儲在合成事件對象上的currentTarget
屬性上,而後將合成事件對象與當前fiber對應的事件處理函數一塊兒傳入invokeGuardedCallbackAndCatchFirstError
:
react\packages\shared\ReactErrorUtils.js
function invokeGuardedCallbackAndCatchFirstError(name,func,context,a,b,c,d,e,f) {
invokeGuardedCallback.apply(this, arguments);
}
export function invokeGuardedCallback(name,func,context,a,b,c,d,e,f) {
invokeGuardedCallbackImpl.apply(reporter, arguments);
}
//在開發環境下invokeGuardedCallbackImpl = invokeGuardedCallbackDev;
//invokeGuardedCallbackDev中涉及到如何處理錯誤,保證了系統的健壯性——TODO
//react\packages\shared\invokeGuardedCallbackImpl.js
let invokeGuardedCallbackImpl = function(name,func,context,a,b,c,d,e,f){
const funcArgs = Array.prototype.slice.call(arguments, 3);
try {
func.apply(context, funcArgs);
} catch (error) {
this.onError(error);
}
};
複製代碼
最終經過invokeGuardedCallbackImpl
中調用func.apply(context, funcArgs)
,執行事件處理函數。
fiber
樹上模擬捕獲與冒泡事件,即先模擬捕獲階段,從遠古祖先節點document
開始向事件觸發的目標節點遍歷,遍歷的過程蒐集須要在捕獲階段觸發的事件處理函數以及對應的節點並存入合成事件對象的相關屬性;而後模擬冒泡階段,從事件觸發的目標節點開始向遠古祖先節點document
開始遍歷,遍歷的過程蒐集須要在冒泡階段觸發的事件處理函數以及對應的節點並存入以前存儲了捕獲階段的函數與節點的相關屬性的數組中;在兩個遍歷之初會遍歷一遍事件目標節點以上的樹並以此存入一個數組中,而後捕獲和冒泡都是遍歷這個數組。經過addEventListener
添加的事件處理函數只有一個輸入參數爲event
事件。 以下:
element.addEventListener(eventType, listener, true);
複製代碼
如今的需求是: 當事件觸發的時候,想調用下面這個函數
function dispatchInteractiveEvent(type, event) {
...
}
複製代碼
最容易想到的方法是:
const listener = function(event){
// 一些和type變量有關的邏輯
...
dispatchInteractiveEvent(type, event)
}
element.addEventListener(eventType, listener, true);
複製代碼
利用bind實現:
// 一些和type變量有關的邏輯
...
const listener = dispatchInteractiveEvent.bind(null, type);
element.addEventListener(eventType, listener, true);
複製代碼
這二者的區別:
bind
實現能夠將type
的邏輯放到外層做用域,而且在事件觸發以前type
就計算好了。而第一種方法事件觸發調用回調的時候才計算type
,這個時候必然形成內存泄漏,由於type
的邏輯可能涉及到外層做用域的局部變量。bind
實現,type
已經計算好了,因此垃圾回收機制會自動回收不用的變量。
總的一句話就是:第一種形成了閉包的效果。
apply
將數組b添加到數組a後面var a=[1,2],b=[3,4]
a.push.apply(a,b) // 返回數組a的長度
a // [1, 2, 3, 4]
b // [3, 4]
複製代碼
apply
在這裏主要的做用是能夠將數組做爲參數傳入,而後一個一個對數組中的元素執行a.push(b[i])
,這裏this必須指向a
。
var a=[1,2],b=[3,4]
a.concat(b) // 返回新的數組,由a與b數組組成
複製代碼
這裏apply
能夠節省內存,不須要建立新的數組。
該函數保持傳入的第二個參數不發生改變
function accumulateInto(
current,
next,
) {
if (current == null) {
return next;
}
if (Array.isArray(current)) {
if (Array.isArray(next)) {
current.push.apply(current, next);
return current;
}
current.push(next);
return current;
}
if (Array.isArray(next)) {
return [current].concat(next);
}
return [current, next];
}
複製代碼