
框架總覽
- ?? DOM事件流的三個階段
- ?? 關(guān)于React事件的疑問
- ?? React事件綁定機制
- ?? React事件和原生事件有什么區(qū)別
- ?? React事件和原生事件的執(zhí)行順序,可以混用嗎
- ?? React事件如何解決跨瀏覽器兼容
- ?? React stopPropagation 與 stopImmediatePropagation
- ?? 從React的事件機制源碼看整個流程
- ?? 基本流程
- ?? 事件注冊
- ?? 事件觸發(fā)
- ?? 總結(jié)
- ?? 站在巨人肩上
DOM事件流的三個階段

1、事件捕獲階段
當(dāng)某個事件觸發(fā)時,文檔根節(jié)點最先接受到事件,然后根據(jù)DOM樹結(jié)構(gòu)向具體綁定事件的元素傳遞。該階段為父元素截獲事件提供了機會。
事件傳遞路徑為:
window —> document —> boy —> div—> text
2、目標階段
具體元素已經(jīng)捕獲事件。之后事件開始向根節(jié)點冒泡。
3、事件冒泡階段
該階段的開始即是事件的開始,根據(jù)DOM樹結(jié)構(gòu)由具體觸發(fā)事件的元素向根節(jié)點傳遞。
事件傳遞路徑:
text—> div —> body —> document —> window
使用addEventListener函數(shù)在事件流的的不同階段監(jiān)聽事件。
DOMEle.addEventListener(‘事件名稱’,handleFn,Boolean);
此處第三個參數(shù)Boolean即代表監(jiān)聽事件的階段;
為true時,在在捕獲階段監(jiān)聽事件,執(zhí)行邏輯處理;
為false時,在冒泡階段監(jiān)聽事件,執(zhí)行邏輯處理。

關(guān)于React事件的疑問
1.React事件綁定機制
考慮到瀏覽器的兼容性和性能問題,React 基于 Virtual DOM 實現(xiàn)了一個SyntheticEvent(合成事件)層,我們所定義的事件處理器會接收到一個SyntheticEvent對象的實例。與原生事件直接在元素上注冊的方式不同的是,react的合成事件不會直接綁定到目標dom節(jié)點上,用事件委托機制,以隊列的方式,從觸發(fā)事件的組件向父組件回溯直到document節(jié)點,因此React組件上聲明的事件最終綁定到了document 上。用一個統(tǒng)一的監(jiān)聽器去監(jiān)聽,這個監(jiān)聽器上保存著目標節(jié)點與事件對象的映射,當(dāng)組件掛載或卸載時,只是在這個統(tǒng)一的事件監(jiān)聽器上插入或刪除一些對象;當(dāng)事件發(fā)生時,首先被這個統(tǒng)一的事件監(jiān)聽器處理,然后在映射里找到真正的事件處理函數(shù)并調(diào)用。這樣做的好處:
1.減少內(nèi)存消耗,提升性能,不需要注冊那么多的事件了,一種事件類型只在 document 上注冊一次
2.統(tǒng)一規(guī)范,解決 ie 事件兼容問題,簡化事件邏輯
3.對開發(fā)者友好
React Event的主要四個文件是 ReactBrowerEventEmitter.js(負責(zé)節(jié)點綁定的回調(diào)函數(shù),該回調(diào)函數(shù)執(zhí)行過程中構(gòu)建合成事件對象,獲取組件實例的綁定回調(diào)并執(zhí)行,若有state變更,則重繪組件),ReactEventListener.js(負責(zé)事件注冊和事件分發(fā)), ReactEventEmitter(負責(zé)事件的執(zhí)行),EventPluginHub.js(負責(zé)事件的存儲)和ReactEventEmitterMixin.js(負責(zé)事件的合成)。
2. React事件和原生事件有什么區(qū)別
帶著問題用以下用代碼來展示兩者的區(qū)別:
- 點擊button,最后的輸出順序是什么?
- B,G 處的type都是啥?
export default class Test extends React.Component {
componentDidMount() {
document.querySelector('#btn').addEventListener('click', (e) => {
console.log('A inner listener')
setTimeout(() => {
console.log('B inner listener timer', e.type)
})
})
document.body.addEventListener('click', (e) => {
console.log('C document listener')
})
window.addEventListener('click', (e) => {
console.log('D window listener')
})
}
outClick(e) {
setTimeout(() => {
console.log('E out timer', e.type)
})
console.log('F out e', e.type)
}
innerClick = (e) => {
console.log('G inner e',e.type)
e.stopPropagation()
}
render() {
return (
<div onClick={this.outClick}>
<button id="btn" onClick={this.innerClick}>點我</button>
</div>
)
}
}
1. 最后的輸出順序為 A C G B
2. B處的type為click,而G處的type為null
響應(yīng)過程(對應(yīng)第一問)
我們參照上題,詳細說一下事件的響應(yīng)過程:
由于我們寫的幾個監(jiān)聽事件addEventListener,都沒有給第三個參數(shù),默認值為false,所以在事件捕獲階段,原生的監(jiān)聽事件沒有響應(yīng),react合成事件只實現(xiàn)了事件冒泡。所以在捕獲階段沒有事件響應(yīng)。
接著到了事件綁定的階段,button上掛載了原生事件,于是輸出"A",setTimeout中的"B"則進入EVENT LOOP。在上一段中,我們提到react的合成事件是掛載到document上,所以“G”沒有輸出。
之后進入冒泡階段,到了div上,與上條同理,不會響應(yīng)outClick,繼續(xù)向上冒泡。
之后冒泡到了document上,先響應(yīng)掛載到document的原生事件,輸出"c"。之后接著由里向外響應(yīng)合成事件隊列,即輸出"G",由于innerClick函數(shù)內(nèi)設(shè)置了e.stopPropagation()。所以阻止了冒泡,父元素的事件響應(yīng)函數(shù)沒有執(zhí)行。React合成事件執(zhí)行e.stopPropagation()不會影響document層級之前的原生事件冒泡。但是會影響document之后的原生事件。所以沒有執(zhí)行body的事件響應(yīng)函數(shù)。之后再處理EVENT LOOP上的事件,輸出'B''.
事件池(對應(yīng)第二問)
在react中,合成事件被調(diào)用后,合成事件對象會被重用,所有屬性被置為null
event.constructor.release(event);
所以題目中outClick中通過異步方式訪問e.type是取不到任何值的,如果需要保留屬性,可以調(diào)用event.persist()事件,會保留引用。
總結(jié)
(1)命名規(guī)范不同
React事件的屬性名是采用駝峰形式的,事件處理函數(shù)是一個函數(shù);
原生事件通過addEventListener給事件添加事件處理函數(shù)
(2)React事件只支持事件冒泡。原生事件通過配置第三個參數(shù),true為事件捕獲,false為事件冒泡
(3)事件掛載目標不同
React事件統(tǒng)一掛載到document上;
原生事件掛載到具體的DOM上
(4)this指向不同
原生事件:
1.如果onevent事件屬性定義的時候?qū)his作為參數(shù),在函數(shù)中獲取到該參數(shù)是DOM對象。用該方法可以獲取當(dāng)前DOM。
2在方法中直接訪問this, this指向當(dāng)前函數(shù)所在的作用域?;蛘哒f調(diào)用函數(shù)的對象。
React事件:
React中this指向一般都期望指向當(dāng)前組件,如果不綁定this,this一般等于undefined。
React事件需要手動為其綁定this具體原因可以參考文章: 為什么需要在 React 類組件中為事件處理程序綁定 this
(5)事件對象不同
原生js中事件對象是原生事件對象,它存在瀏覽器兼容性,需要用戶自己處理各瀏覽器兼容問題;
ReactJS中的事件對象是React將原生事件對象(event)進行了跨瀏覽器包裝過的合成事件(SyntheticEvent)。
為了性能考慮,執(zhí)行完后,合成事件的事件屬性將不能再訪問
React事件和原生事件的執(zhí)行順序,可以混用嗎
由上面的代碼我們可以理解:
react的所有事件都掛載在document中
當(dāng)真實dom觸發(fā)后冒泡到document后才會對react事件進行處理
所以原生的事件會先執(zhí)行
然后執(zhí)行react合成事件
最后執(zhí)行真正在document上掛載的事件
不要將合成事件與原生事件混用。執(zhí)行React事對象件的e.stopPropagation()可以阻止React事件冒泡。但是不能阻止原生事件冒泡;反之,在原生事件中的阻止冒泡行為,卻可以阻止 React 合成事件的傳播。因為無法將事件冒泡到document上導(dǎo)致的
React事件如何解決跨瀏覽器兼容
react事件在給document注冊事件的時候也是對兼容性做了處理。

上面這個代碼就是給document注冊事件,內(nèi)部其實也是做了對ie瀏覽器的兼容做了處理。
其實react內(nèi)部還處理了很多,比如react合成事件:
React根據(jù) W3C 規(guī)范 定義了這個合成事件,所以你不需要擔(dān)心跨瀏覽器的兼容性問題。
事件處理程序?qū)鬟f SyntheticEvent 的實例,這是一個跨瀏覽器原生事件包裝器。 它具有與瀏覽器原生事件相同的接口,包括stopPropagation() 和 preventDefault() ,在所有瀏覽器中他們工作方式都相同。
每個SyntheticEvent對象都具有以下屬性:
| 屬性名 | 類型 | 描述 |
|---|---|---|
| bubbles | boolean | 事件是否可冒泡 |
| cancelable | boolean | 事件是否可擁有取消的默認動作 |
| currentTarget | DOMEventTarget | 事件監(jiān)聽器觸發(fā)該事件的元素(綁定事件的元素) |
| defaultPrevented | boolean | 當(dāng)前事件是否調(diào)用了 event.preventDefault()方法 |
| eventPhase | number | 事件傳播的所處階段[0:Event.NONE-沒有事件被處理,1:Event.CAPTURING_PHASE - 捕獲階段,2:被目標元素處理,3:冒泡階段(Event.bubbles為true時才會發(fā)生)] |
| isTrusted | boolean | 觸發(fā)是否來自于用戶行為,false為腳本觸發(fā) |
| nativeEvent | DOMEvent | 瀏覽器原生事件 |
| preventDefault() | void | 阻止事件的默認行為 |
| isDefaultPrevented() | boolean | 返回的事件對象上是否調(diào)用了preventDefault()方法 |
| stopPropagation() | void | 阻止冒泡 |
| isPropagationStopped() | boolean | 返回的事件對象上是否調(diào)用了stopPropagation()方法 |
| target | DOMEventTarget | 觸發(fā)事件的元素 |
| timeStamp | number | 事件生成的日期和時間 |
| type | string | 當(dāng)前 Event 對象表示的事件的名稱,是注冊事件的句柄,如,click、mouseover...etc. |
React合成的SyntheticEvent采用了事件池,這樣做可以大大節(jié)省內(nèi)存,而不會頻繁的創(chuàng)建和銷毀事件對象。
另外,不管在什么瀏覽器環(huán)境下,瀏覽器會將該事件類型統(tǒng)一創(chuàng)建為合成事件,從而達到了瀏覽器兼容的目的。
React stopPropagation 與 stopImmediatePropagation
React 合成事件與原生事件執(zhí)行順序圖:

從圖中我們可以得到一下結(jié)論:
(1)DOM 事件冒泡到document上才會觸發(fā)React的合成事件,所以React 合成事件對象的e.stopPropagation,只能阻止 React 模擬的事件冒泡,并不能阻止真實的 DOM 事件冒泡
(2)DOM 事件的阻止冒泡也可以阻止合成事件原因是DOM 事件的阻止冒泡使事件不會傳播到document上
(3)當(dāng)合成事件和DOM 事件 都綁定在document上的時候,React的處理是合成事件應(yīng)該是先放進去的所以會先觸發(fā),在這種情況下,原生事件對象的 stopImmediatePropagation能做到阻止進一步觸發(fā)document DOM事件
stopImmediatePropagation :如果有多個相同類型事件的事件監(jiān)聽函數(shù)綁定到同一個元素,則當(dāng)該類型的事件觸發(fā)時,它們會按照被添加的順序執(zhí)行。如果其中某個監(jiān)聽函數(shù)執(zhí)行了 event.stopImmediatePropagation()方法,則剩下的監(jiān)聽函數(shù)將不會被執(zhí)行。
從React的事件機制源碼看整個流程

基本流程
在 react源碼的 react-dom/src/events/ReactBrowserEventEmitter.js文件的開頭,有這么一大段注釋:
/**
* Summary of `ReactBrowserEventEmitter` event handling:
*
* - Top-level delegation is used to ......
* ......
*
* +------------+ .
* | DOM | .
* +------------+ .
* | .
* v .
* +------------+ .
* | ReactEvent | .
* | Listener | .
* +------------+ . +-----------+
* | . +--------+|SimpleEvent|
* | . | |Plugin |
* +-----|------+ . v +-----------+
* | | | . +--------------+ +------------+
* | +-----------.--->|EventPluginHub| | Event |
* | | . | | +-----------+ | Propagators|
* | ReactEvent | . | | |TapEvent | |------------|
* | Emitter | . | |<---+|Plugin | |other plugin|
* | | . | | +-----------+ | utilities |
* | +-----------.--->| | +------------+
* | | | . +--------------+
* +-----|------+ . ^ +-----------+
* | . | |Enter/Leave|
* + . +-------+|Plugin |
* +-------------+ . +-----------+
* | application | .
* |-------------| .
* | | .
* | | .
* +-------------+ .
* .
* React Core . General Purpose Event Plugin System
*/
這段注釋是在大概描述 React的事件機制,也就是這個文件中的代碼要做的一些事情,大概意思就是說事件委托是很常用的一種瀏覽器事件優(yōu)化策略,于是 React就接管了這件事情,并且還貼心地消除了瀏覽器間的差異,賦予開發(fā)者跨瀏覽器的開發(fā)體驗,主要是使用 EventPluginHub這個東西來負責(zé)調(diào)度事件的存儲,合成事件并以對象池的方式實現(xiàn)創(chuàng)建和銷毀。
React內(nèi)部事件系統(tǒng)實現(xiàn)可以分為兩個階段:事件注冊、事件觸發(fā),涉及的主要類如下:
ReactEventListener:負責(zé)事件注冊和事件分發(fā)。React將DOM事件全都注冊到document節(jié)點上,事件分發(fā)主要調(diào)用dispatchEvent進行,從事件觸發(fā)組件開始,向父元素遍歷。
ReactEventEmitter:負責(zé)每個組件上事件的執(zhí)行。
EventPluginHub:負責(zé)回調(diào)函數(shù)的存儲
JSX中聲明一個React事件,比如:
render() {
return (
<button onClick={this.handleClick}>點擊</button>
)
}
用戶點擊button按鈕觸發(fā)click事件后,DOM將event傳給ReactEventListener,觸發(fā)document上注冊的事件處理函數(shù),執(zhí)行ReactEventListener.dispatchEvent(event)將事件分發(fā)到當(dāng)前組件及以上的父組件。然后ReactEventEmitter對每個組件進行事件的執(zhí)行,先構(gòu)造React合成事件,然后以隊列的方式調(diào)用JSX中聲明的callback。

備注:以下代碼邏輯大部分寫在注釋里面
事件注冊
這是 react 事件機制的第1步 - 事件注冊,在這里你將了解react事件的注冊過程,以及在這個過程中主要經(jīng)過了哪些關(guān)鍵步驟,同時結(jié)合源碼進行驗證和增強理解。
在這里并不會說非常細節(jié)的內(nèi)容,而是把大概的流程和原理性的內(nèi)容進行介紹,做到對整體流程有個認知和理解。
大致流程
react 事件注冊過程其實主要做了2件事:事件注冊、事件存儲。
a. 事件注冊 - 組件掛載階段,根據(jù)組件內(nèi)的聲明的事件類型-onclick,onchange 等,給 document 上添加事件 -addEventListener,并指定統(tǒng)一的事件處理程序 dispatchEvent。
b. 事件存儲 - 就是把 react 組件內(nèi)的所有事件統(tǒng)一的存放到一個對象里,緩存起來,為了在觸發(fā)事件的時候可以查找到對應(yīng)的方法去執(zhí)行。

關(guān)鍵步驟
上面大致說了事件注冊需要完成的兩個目標,那完成目標的過程需要經(jīng)過哪些關(guān)鍵處理呢?
首先 react 拿到將要掛載的組件的虛擬 dom(其實就是 react element 對象),然后處理react dom 的 props ,判斷屬性內(nèi)是否有聲明為事件的屬性,比如onClick,onChange,這個時候得到事件類型 click,change 和對應(yīng)的事件處理程序 fn,然后執(zhí)行后面3步
a. 完成事件注冊
b. 將react dom ,事件類型,處理函數(shù) fn 放入數(shù)組存儲
c. 組件掛載完成后,處理 b 步驟生成的數(shù)組,經(jīng)過遍歷把事件處理函數(shù)存儲到listenerBank(一個對象)中

源碼解析

1.從 jsx 說起
看個最熟悉的代碼,也是我們?nèi)粘5膶懛?/p>
//此處代碼省略
handleFatherClick=()=>{
}
handleChildClick=()=>{
}
render(){
return <div className="box">
<div className="father" onClick={this.handleFatherClick}>
<div className="child" onClick={this.handleChildClick}>child </div>
</div>
</div>
}
經(jīng)過 babel 編譯后,可以看到最終調(diào)用的方法是react.createElement,而且聲明的事件類型和回調(diào)就是個props。
react.createElement('div',
{
className:'box',
},
react.createElement('div',
{
className:'father',
onClick: this.handleFatherClick
},
react.createElement('div',
{
className:'child',
onClick: this.handleChildClick,
},
'child'
);
);
);
react.createElement執(zhí)行的結(jié)果會返回一個所謂的虛擬 dom (react element object)

處理組件props,拿到事件類型和回調(diào) fn
ReactDOMComponent在進行組件加載(mountComponent)、更新(updateComponent)的時候,需要對props進行處理(_updateDOMProperties):
_updateDOMProperties: function (lastProps, nextProps, transaction) {
... // 前面代碼太長,省略一部分
for (propKey in nextProps) {
var nextProp = nextProps[propKey];
if(propKey === STYLE){
...
}else if(registrationNameModules.hasOwnProperty(propKey)){
// 如果是props這個對象直接聲明的屬性,而不是從原型鏈中繼承而來的,則處理它
// nextProp表示要創(chuàng)建或者更新的屬性,而lastProp則表示上一次的屬性
// 對于mountComponent,lastProp為null。updateComponent二者都不為null。unmountComponent則nextProp為null
if (nextProp) {
// mountComponent和updateComponent中,enqueuePutListener注冊事件
enqueuePutListener(this, propKey, nextProp, transaction);
} else if (lastProp) {
// unmountComponent中,刪除注冊的listener,防止內(nèi)存泄漏
deleteListener(this, propKey);
}
}else{
...
}
}
}
可以看下 registrationNameModules 的內(nèi)容,就不細說了,他就是一個內(nèi)置的常量。

事件注冊和事件的存儲
1.事件注冊

接著上面的代碼執(zhí)行到了這個方法
enqueuePutListener(this, propKey, nextProp, transaction);
在這個方法里會進行事件的注冊以及事件的存儲,包括冒泡和捕獲的處理
// inst: React Component對象
// registrationName: React合成事件名,如onClick
// listener: React事件回調(diào)方法,如onClick=callback中的callback
// transaction: mountComponent或updateComponent所處的事務(wù)流中,React都是基于事務(wù)流的
function enqueuePutListener(inst, registrationName, listener, transaction) {
if (transaction instanceof ReactServerRenderingTransaction) {
return;
}
var containerInfo = inst._hostContainerInfo;
var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
// 找到document
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
// 注冊事件,將事件注冊到document上
listenTo(registrationName, doc);
// 存儲事件,放入事務(wù)隊列中
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener
});
}
可以看到這個函數(shù)一共做了三件事:
1.根據(jù)當(dāng)前的組件實例獲取到最高父級-也就是document;
2.然后執(zhí)行方法 listenTo - 也是最關(guān)鍵的一個方法,進行事件注冊。
- 最后執(zhí)行transaction.getReactMountReady().enqueue,將react dom 實例,事件類型,處理函數(shù) fn 組成一個對象放入數(shù)組存儲。等待組件掛載后依次為數(shù)組里面每一項執(zhí)行putListener。為數(shù)組每一項生成一個映射關(guān)系,把這個關(guān)系保存在了一個 map里,也就是一個對象(鍵值對),然后在事件觸發(fā)的時候去根據(jù)當(dāng)前的組件id和事件類型查找到對應(yīng)的事件fn。
從ReactBrowserEventEmitter.listenTo;在ReactBrowserEventEmitter
.js文件下找到listenTo方法,可以發(fā)現(xiàn)它主要解決了不同瀏覽器間捕獲和冒泡不兼容的問題。click,mousewheel等事件調(diào)用trapBubbledEvent來注冊冒泡事件;scroll,focus等事件調(diào)用trapCapturedEvent來注冊捕獲事件。
listenTo : function (registrationName, contentDocumentHandle) {
var mountAt = contentDocumentHandle;
var isListening = getListeningForDocument(mountAt);
var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];
for (var i = 0; i < dependencies.length; i++) {
var dependency = dependencies[i];
if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
if (dependency === 'topWheel') {
if (isEventSupported('wheel')) {
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'wheel', mountAt);
} else if (isEventSupported('mousewheel')) {
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'mousewheel', mountAt);
} else {
// Firefox needs to capture a different mouse scroll event.
// @see http://www.quirksmode.org/dom/events/tests/scroll.html
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'DOMMouseScroll', mountAt);
}
} else if (dependency === 'topScroll') {
if (isEventSupported('scroll', true)) {
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topScroll', 'scroll', mountAt);
} else {
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topScroll', 'scroll', ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE);
}
} else if (dependency === 'topFocus' || dependency === 'topBlur') {
if (isEventSupported('focus', true)) {
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topFocus', 'focus', mountAt);
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topBlur', 'blur', mountAt);
} else if (isEventSupported('focusin')) {
// IE has `focusin` and `focusout` events which bubble.
// @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topFocus', 'focusin', mountAt);
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topBlur', 'focusout', mountAt);
}
// to make sure blur and focus event listeners are only attached once
isListening.topBlur = true;
isListening.topFocus = true;
} else if (topEventMapping.hasOwnProperty(dependency)) {
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
}
isListening[dependency] = true;
}
}
}
最后執(zhí)行EventListener.listen(冒泡)或者EventListener.capture(捕獲),來看看將事件綁定到冒泡階段的具體代碼:
// 三個參數(shù)為 topEvent、原生 DOM Event、Document(掛載節(jié)點)
trapBubbledEvent: function (topLevelType, handlerBaseName, element) {
if (!element) {
return null;
}
return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
}
// 三個參數(shù)為 Document(掛載節(jié)點)、原生 DOM Event、事件綁定函數(shù)
listen: function listen(target, eventType, callback) {
if (target.addEventListener) {
target.addEventListener(eventType, callback, false);
// 返回一個解綁的函數(shù)
return {
remove: function remove() {
target.removeEventListener(eventType, callback, false);
}
}
}
if (target.attachEvent) {
target.attachEvent('on' + eventType, callback);
// 返回一個解綁的函數(shù)
return {
remove: function remove() {
target.detachEvent('on' + eventType, callback);
}
}
}
}
也可以看到注冊事件的時候也對 ie 瀏覽器做了兼容。
上面沒有看到 dispatchEvent 的定義,其實上面代碼中的callback統(tǒng)一為dispatchEvent。dispatchEvent將在之后講。
到這里事件注冊就完事兒了。
事件存儲
開始事件的存儲,在 react 里所有事件的觸發(fā)都是通過 dispatchEvent方法統(tǒng)一進行派發(fā)的,而不是在注冊的時候直接注冊聲明的回調(diào),來看下事件如何存儲的 。

還是上面的源碼:
function enqueuePutListener(inst, registrationName, listener, transaction) {
var containerInfo = inst._hostContainerInfo;
var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
listenTo(registrationName, doc);//這個方法上面已說完
//這里涉及到了事務(wù),事物會在以后的章節(jié)再介紹,主要看事件注冊
//下面的代碼是將putListener放入數(shù)組,當(dāng)組件掛載完后會依次執(zhí)行數(shù)組的回調(diào)。也就是putListener會依次執(zhí)行
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,//組件實例
registrationName: registrationName,//事件類型 click
listener: listener //事件回調(diào) fn
});
}
大致的流程就是執(zhí)行完listenTo(事件注冊),執(zhí)行transaction.getReactMountReady().enqueue,將react dom 實例,事件類型,處理函數(shù) fn 組成一個對象放入數(shù)組存儲。等待組件掛載后依次為數(shù)組里面每一項執(zhí)行putListener。為數(shù)組每一項生成一個映射關(guān)系,把這個關(guān)系保存在了一個 對象(鍵值對)里,這個對象叫做 listenerBank,如下圖。然后在事件觸發(fā)的時候去根據(jù)當(dāng)前的組件id和事件類型查找到對應(yīng)的事件fn。

事件存儲由EventPluginHub來負責(zé),EventPluginHub在react事件系統(tǒng)的核心文件renderers/shared/event/EventPluginHub.js中定義,感興趣的同學(xué)可以去看看源碼~~
var EventPluginHub = {
injection,
putListener,
getListener,
deleteListener,
deleteAllListeners,
extractEvents, // 當(dāng)頂層事件被觸發(fā),該方法中會傳入原生事件,生成合成事件
enqueueEvents,// 合成事件進入事件隊列
processEventQueue, // 調(diào)度事件隊列上的所有合成事件
}
事件存儲的入口在我們上面講到的putListener方法,如下
function putListener() {
var listenerToPut = this;
EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
}
實際調(diào)用的是EventPluginHub.js中的putListener方法,該方法在組件掛載時執(zhí)行。EventPluginHub.js主要負責(zé)事件的存儲、合成事件以對象池的方式實現(xiàn)創(chuàng)建和銷毀。
/**
* EventPluginHub用來存儲React事件, 將listener存儲到`listenerBank[registrationName][key]`
*
* @param {object} inst: 事件源
* @param {string} listener的名字,比如onClick
* @param {function} listener的callback
*/
//
var listenerBank = {};
putListener: function (inst, registrationName, listener) {
// 用來標識注冊了事件,比如onClick的React對象。key的格式為'.nodeId', 只用知道它可以標示哪個React對象就可以了
var key = getDictionaryKey(inst);
var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
// 將listener事件回調(diào)方法存入listenerBank[registrationName][key]中,比如listenerBank['onclick'][nodeId]
// 所有React組件對象定義的所有React事件都會存儲在listenerBank中
bankForRegistrationName[key] = listener;
//onSelect和onClick注冊了兩個事件回調(diào)插件, 用于walkAround某些瀏覽器兼容bug,不用care
var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
if (PluginModule && PluginModule.didPutListener) {
PluginModule.didPutListener(inst, registrationName, listener);
}
},
var getDictionaryKey = function (inst) {
return '.' + inst._rootNodeID;
};
listenerBank其實就是一個二級 map,這樣的結(jié)構(gòu)更方便事件的查找。
這里的組件 id 就是組件的唯一標識,然后和fn進行關(guān)聯(lián),在觸發(fā)階段就可以找到相關(guān)的事件回調(diào)。

看到這個結(jié)構(gòu)是不是很熟悉呢?就是我們平常使用的 object.
到這里大致的流程已經(jīng)說完,是不是感覺有點明白又不大明白。
沒關(guān)系,再來個詳細的圖,重新理解下。

事件觸發(fā)
在事件注冊階段,最終所有的事件和事件類型都會保存到listenerBank中。
那么在事件觸發(fā)的過程中上面這個對象有什么用處呢?
其實就是用來查找事件回調(diào)
事件觸發(fā)過程總結(jié)為主要分為3個步驟:事件分發(fā)、生成合成事件、批量執(zhí)行事件回調(diào)
1.進入統(tǒng)一的事件分發(fā)函數(shù)(dispatchEvent)
2.結(jié)合原生事件找到當(dāng)前節(jié)點對應(yīng)的ReactDOMComponent對象
3.開始事件的合成
3.1 根據(jù)當(dāng)前事件類型生成指定的合成對象
3.2 封裝原生事件和冒泡機制
3.3 查找當(dāng)前元素以及他所有父級
3.4 在listenerBank查找事件回調(diào)并合成到 event(合成事件結(jié)束)
4.批量處理合成事件內(nèi)的回調(diào)事件(事件觸發(fā)完成 end)


舉個栗子
在說具體的流程前,先看一個栗子,后面的分析也是基于這個栗子
handleFatherClick=(e)=>{
console.log('father click');
}
handleChildClick=(e)=>{
console.log('child click');
}
render(){
return <div className="box">
<div className="father" onClick={this.handleFatherClick}> father
<div className="child" onClick={this.handleChildClick}>child </div>
</div>
</div>
}
看到這個熟悉的代碼,我們就已經(jīng)知道了執(zhí)行結(jié)果。
當(dāng)我點擊 child div 的時候,會同時觸發(fā)father的事件。

1. 事件分發(fā)

當(dāng)事件觸發(fā)時,注冊在document上的回調(diào)函數(shù)會被觸發(fā)。事件觸發(fā)的入口函數(shù)是ReactEventListener.dispatchEvent負責(zé)分發(fā)已經(jīng)注冊的回調(diào)函數(shù)。在這個函數(shù)中會調(diào)用batchingStrategy 的 batchUpdate 方法實現(xiàn)批量處理更新。batchUpdate以transaction形式調(diào)用,批量處理更新。
// topLevelType:帶top的事件名,如topClick。不用糾結(jié)為什么帶一個top字段,知道它是事件名就OK了
// nativeEvent: 用戶觸發(fā)click等事件時,瀏覽器傳遞的原生事件
dispatchEvent: function (topLevelType, nativeEvent) {
// disable了則直接不回調(diào)相關(guān)方法
if (!ReactEventListener._enabled) {
return;
}
// bookKeeping的作用看ta的定義就知道了,就是一個用來保存過程中會使用到的變量的對象。使用了
//react在源碼中用到的對象池的方法來避免多余的垃圾回收
function TopLevelCallbackBookKeeping(topLevelType, nativeEvent) {
this.topLevelType = topLevelType;
this.nativeEvent = nativeEvent;
this.ancestors = [];
}
Object.assign(TopLevelCallbackBookKeeping.prototype, {
destructor: function() {
this.topLevelType = null;
this.nativeEvent = null;
this.ancestors.length = 0;
},
});
PooledClass.addPoolingTo(
TopLevelCallbackBookKeeping,
PooledClass.twoArgumentPooler
);
var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
try {
// 放入批處理隊列中,React事件流也是一個消息隊列的方式
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
}
關(guān)于getPooled,可以參考我的另一篇文章react 對象池解讀
TopLevelCallbackBookKeeping是一個類,該類對象用于記錄topLevelType,nativeEvent和用于存儲所有的祖先節(jié)點數(shù)組ancestors(當(dāng)前是空的,只有分發(fā)時才會遍歷并存儲所有的祖先節(jié)點) 。
那么傳入batchedUpdates 內(nèi)部的回調(diào)函數(shù)handleTopLevelImpl是什么呢???它其實就是事件分發(fā)的核心部分。
// document進行事件分發(fā),這樣具體的React組件才能得到響應(yīng)。因為DOM事件是綁定到document上的
function handleTopLevelImpl(bookKeeping) {
// 獲取發(fā)生原生的事件的e.target
var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
// // 獲取原生事件的target說在的組件,它是虛擬DOM
var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
// 執(zhí)行事件回調(diào)前,先由當(dāng)前組件向上遍歷它的所有父組件。得到ancestors這個數(shù)組。
// 因為事件回調(diào)中可能會改變Virtual DOM結(jié)構(gòu),所以要先遍歷好組件層級
var ancestor = targetInst;
do {
bookKeeping.ancestors.push(ancestor);
// 這里的findParent曾經(jīng)給我?guī)碚`導(dǎo),我以為去找當(dāng)前元素所有的父節(jié)點,但其實不是的,
// 我們知道一般情況下,我們的組件最后會被包裹在<div id='root'></div>的標簽里
// 一般是沒有組件再去嵌套它的,所以通常返回null
ancestor = ancestor && findParent(ancestor);
} while (ancestor);
// 從當(dāng)前組件向父組件遍歷,依次執(zhí)行注冊的回調(diào)方法. 我們遍歷構(gòu)造ancestors數(shù)組時,是從當(dāng)前組件向父組件回溯的,故此處事件回調(diào)也是這個順序
// 這個順序就是冒泡的順序
for (var i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
}
}
function findParent(inst) {
while (inst._hostParent) {
inst = inst._hostParent;
}
var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst);
var container = rootNode.parentNode;
return ReactDOMComponentTree.getClosestInstanceFromNode(container);
}
從上面的事件分發(fā)中可見,React自身實現(xiàn)了一套冒泡機制。從觸發(fā)事件的對象開始,向父元素回溯,依次調(diào)用它們注冊的事件callback。
看下ReactDOMComponent實例的內(nèi)容

事件處理由_handleTopLevel完成。它其實是調(diào)用ReactBrowserEventEmitter.handleTopLevel() ,如下
// React事件調(diào)用的入口。DOM事件綁定在了document原生對象上,每次事件觸發(fā),都會調(diào)用到handleTopLevel
handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
// 采用對象池的方式構(gòu)造出合成事件。不同的eventType的合成事件可能不同
var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
// 批處理隊列中的events
runEventQueueInBatch(events);
}
handleTopLevel方法是事件callback調(diào)用的核心。它主要做兩件事情,一方面利用瀏覽器回傳的原生事件構(gòu)造出React合成事件,另一方面采用隊列的方式處理events。先看如何構(gòu)造合成事件。
2. 事件合成

合成事件時一個跨瀏覽器原生事件包裝器,具有與瀏覽器原生事件相同的接口,包括 stopPropagation() 和 preventDefault() ,除了事件在所有瀏覽器中他們工作方式都相同?,F(xiàn)在來看一看React是如何實現(xiàn)合成事件的。
EventPluginHub.js中extractEvents方法:
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
var events;
var plugins = EventPluginRegistry.plugins;
for (var i = 0; i < plugins.length; i++) {
// Not every plugin in the ordering may be loaded at runtime.
var possiblePlugin = plugins[i];
if (possiblePlugin) {
var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
}
}
}
return events;
}
插件中的extractevents
注意不要將EventPluginHub.extractevents和possiblePlugin.extractEvents搞混了
以點擊事件click的生成插件SimpleEventPlugin為例:
//進行事件合成,根據(jù)事件類型獲得指定的合成類
var SimpleEventPlugin = {
eventTypes: eventTypes,
extractEvents: function extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
//代碼已省略....
var EventConstructor;
switch (topLevelType) {
//代碼已省略....
case 'topClick'://【這里有一個不解的地方】 topLevelType = topClick,執(zhí)行到這里了,但是這里沒有做任何操作
if (nativeEvent.button === 2) {
return null;
}
//代碼已省略....
case 'topContextMenu'://而是會執(zhí)行到這里,獲取到鼠標合成類
EventConstructor = SyntheticMouseEvent;
break;
case 'topAnimationEnd':
case 'topAnimationIteration':
case 'topAnimationStart':
EventConstructor = SyntheticAnimationEvent;//動畫類合成事件
break;
case 'topWheel':
EventConstructor = SyntheticWheelEvent;//鼠標滾輪類合成事件
break;
case 'topCopy':
case 'topCut':
case 'topPaste':
EventConstructor = SyntheticClipboardEvent;
break;
}
// 合成事件對象都是以pool方式創(chuàng)建和銷毀的,這提高了React的性能,同時也意味著一旦事件執(zhí)行結(jié)束
// 該合成事件對象會被銷毀。因此不能通過異步方式獲取該事件
var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
EventPropagators.accumulateTwoPhaseDispatches(event);
return event;//最終會返回合成的事件對象
}
EventPluginHub.extractEvents()方法是通過執(zhí)行各個插件的extractEvents方法來創(chuàng)建合成事件。possiblePlugin.extractEvents()根據(jù)事件的Type創(chuàng)建不同插件的合成事件,accumulateInto()負責(zé)將所有插件的合成事件存入到events數(shù)組中,形成當(dāng)前事件的合成事件集合。EventPluginRegistry.plugins默認包含五種plugin,他們是在EventPluginHub初始化階段注入進去的,分別是 SimpleEventPlugin、EnterLeaveEventPlugin、ChangeEventPlugin、SelectEventPlugin和BeforeInputEventPlugin。根據(jù)不同的事件類型采用不同的事件合成方法,這些事件合成方法有SyntheticAnimationEvent.js、SyntheticFocusEvent.js、SyntheticKeyboardEvent.js、SyntheticMouseEvent.js、SyntheticTouchEvent.js、SyntheticUIEvent.js、SyntheticWheelEvent.js等等一共13種合成方法。
上面提到調(diào)用EventPropagators.accumulateTwoPhaseDispatches(event)從EventPluginHub中獲取回調(diào)函數(shù),存儲到合成事件的_dispatchListeners屬性中。如下:
// EventPropagators.js
function accumulateTwoPhaseDispatches(events) {
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}
// forEachAccumulated 函數(shù)在接下來會講到
function accumulateTwoPhaseDispatchesSingle(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
EventPluginUtils.traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
}
}
把所有父級元素綁定的相關(guān)事件按照捕獲->冒泡的順序存push到合成事件對象的_dispatchListeners屬性中。該屬性為一個數(shù)組。
/**
*
* @param {obj} inst 當(dāng)前節(jié)點實例
* @param {function} fn 處理方法
* @param {obj} arg 合成事件對象
*/
function traverseTwoPhase(inst, fn, arg) {
var path = [];//存放所有實例 ReactDOMComponent
while (inst) {
path.push(inst);
inst = inst._hostParent;//層級關(guān)系
}
var i;
for (i = path.length; i-- > 0;) {
fn(path[i], 'captured', arg);//處理捕獲 ,反向處理數(shù)組
}
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);//處理冒泡,從0開始處理,我們直接看冒泡
}
}
看下 path 長啥樣

緊接著如何在listenerBank中查找事件回調(diào)并合成到合成對象的_dispatchListeners中呢。
緊接著上面代碼
fn(path[i], 'bubbled', arg);
上面的代碼會調(diào)用下面這個方法,在listenerBank中查找到事件回調(diào),并存入合成事件對象。
/**EventPropagators.js
* 查找事件回調(diào)后,把實例和回調(diào)保存到合成對象內(nèi)
* @param {obj} inst 組件實例
* @param {string} phase 事件類型
* @param {obj} event 合成事件對象
*/
function accumulateDirectionalDispatches(inst, phase, event) {
var listener = listenerAtPhase(inst, event, phase);
if (listener) {//如果找到了事件回調(diào),則保存起來 (保存在了合成事件對象內(nèi))
event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);//把事件回調(diào)進行合并返回一個新數(shù)組
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);//把組件實例進行合并返回一個新數(shù)組
}
}
/**
* EventPropagators.js
* 中間調(diào)用方法 拿到實例的回調(diào)方法
* @param {obj} inst 實例
* @param {obj} event 合成事件對象
* @param {string} propagationPhase 名稱,捕獲capture還是冒泡bubbled
*/
function listenerAtPhase(inst, event, propagationPhase) {
var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
return getListener(inst, registrationName);
}
/**EventPluginHub.js
* 拿到實例的回調(diào)方法
* @param {obj} inst 組件實例
* @param {string} registrationName Name of listener (e.g. `onClick`).
* @return {?function} 返回回調(diào)方法
*/
getListener: function getListener(inst, registrationName) {
var bankForRegistrationName = listenerBank[registrationName];
if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
return null;
}
var key = getDictionaryKey(inst);
return bankForRegistrationName && bankForRegistrationName[key];
}
為什么能夠查找到的呢?
因為 inst (組件實例)里有_rootNodeID,所以也就有了對應(yīng)關(guān)系。比如通過上面函數(shù)getDictionaryKey獲取到觸發(fā)事件的DOM組件的_rootNodeId屬性,然后根據(jù)callback = listenerBank[eventType][_rootNodeId]可以獲取該組件的回調(diào)函數(shù)。

到這里事件合成對象生成完成,所有的事件回調(diào)已保存到了合成對象中。
3.批量執(zhí)行事件回調(diào)
事件合成之后就需要執(zhí)行事件回調(diào)函數(shù),React以批量處理事件隊列的方式執(zhí)行事件的。它的入口函數(shù)是在ReactEventEmitterMixin.js中的runEventQueueInBatch方法:
var eventQueue = null;
function runEventQueueInBatch(events) {
EventPluginHub.enqueueEvents(events);
EventPluginHub.processEventQueue(false);
}
//EventPluginHub.enqueueEvents()方法是將合成事件寫入隊列,EventPluginHub.processEventQueue()方法是執(zhí)行隊列中的事件。
// EventPluginHub.enqueueEvents()方法的實現(xiàn)邏輯很簡單,使用accumulateInto方法將events存入eventQueue隊列中。
enqueueEvents: function (events) {
if (events) {
eventQueue = accumulateInto(eventQueue, events);
}
}
function accumulateInto(current, next) {
if (current == null) {
return next;
}
// 將next添加到current中,返回一個包含他們兩個的新數(shù)組
// 如果next是數(shù)組,current不是數(shù)組,采用push方法,否則采用concat方法
// 如果next不是數(shù)組,則返回一個current和next構(gòu)成的新數(shù)組
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];
}
// EventPluginHub.processEventQueue()方法的實現(xiàn)邏輯分為:
processEventQueue: function (simulated) {
// 現(xiàn)將eventQueue 設(shè)置為null,以便在處理過程中讓新合成事件入隊列
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 handlers threw.
ReactErrorUtils.rethrowCaughtError();
}
(1)獲取合成事件隊列,其中可能包含之前沒處理完的合成事件
(2)遍歷合成事件隊列,
function forEachAccumulated(arr, cb, scope) {
if (Array.isArray(arr)) {
arr.forEach(cb, scope);
} else if (arr) {
cb.call(scope, arr);
}
}
事件隊列的回調(diào)函數(shù)為executeDispatchesAndReleaseSimulated,負責(zé)事件的分發(fā)和事件遍歷結(jié)束后是否釋放合成事件對象。
var executeDispatchesAndReleaseSimulated = function (e) {
return executeDispatchesAndRelease(e, true);
};
/**/
var executeDispatchesAndRelease = function (event, simulated) {
if (event) {
/*按存儲順序分發(fā)事件,先進先出*/
EventPluginUtils.executeDispatchesInOrder(event, simulated);
/*判斷是否釋放事件*/
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
var executeDispatchesAndReleaseTopLevel = function (e) {
return executeDispatchesAndRelease(e, false);
};
var executeDispatchesAndRelease = function (event, simulated) {
if (event) {
//進行事件分發(fā)
EventPluginUtils.executeDispatchesInOrder(event, simulated);
if (!event.isPersistent()) {
// 處理完,則release掉event對象,采用對象池方式,減少GC
// React幫我們處理了合成事件的回收機制,不需要我們關(guān)心。但要注意,如果使用了DOM原生事件,則要自己回收
event.constructor.release(event);
}
}
};
// EventPluginUtils.js
// 事件處理的核心
function executeDispatchesInOrder(event, simulated) {
var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._dispatchInstances;
if (Array.isArray(dispatchListeners)) {
// 如果有多個listener,則遍歷執(zhí)行數(shù)組中event
for (var i = 0; i < dispatchListeners.length; i++) {
// 如果isPropagationStopped設(shè)成true了,則停止事件傳播,退出循環(huán)。
if (event.isPropagationStopped()) {
break;
}
// 執(zhí)行event的分發(fā),從當(dāng)前觸發(fā)事件元素向父元素遍歷
// event為瀏覽器上傳的原生事件
// dispatchListeners[i]為JSX中聲明的事件callback
// dispatchInstances[i]為對應(yīng)的React Component
executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
}
} else if (dispatchListeners) {
// 如果只有一個listener,則直接執(zhí)行事件分發(fā)
executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
}
// 處理完event,重置變量。因為使用的對象池,故必須重置,這樣才能被別人復(fù)用
event._dispatchListeners = null;
event._dispatchInstances = null;
}
通過executeDispatchesInOrder函數(shù)可知,dispatch 合成事件分為兩個步驟:
- 通過
_dispatchListeners里得到所有綁定的回調(diào)函數(shù),在通過_dispatchInstances的綁定回調(diào)函數(shù)的虛擬dom元素 - 循環(huán)執(zhí)行
_dispatchListeners里所有的回調(diào)函數(shù),這里有一個特殊情況,也是react阻止冒泡的原理
當(dāng)回調(diào)函數(shù)里使用了stopPropagation會使得數(shù)組后面的回調(diào)函數(shù)不能執(zhí)行,這樣就做到了阻止事件冒泡
目前還是還有看到執(zhí)行事件的代碼,在接著看:
/**
*
* @param {obj} event 合成事件對象
* @param {boolean} simulated false
* @param {fn} listener 事件回調(diào)
* @param {obj} inst 組件實例
*/
function executeDispatch(event, simulated, listener, inst) {
var type = event.type || 'unknown-event';
event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
if (simulated) {//調(diào)試環(huán)境的值為 false,按說生產(chǎn)環(huán)境是 true
//方法的內(nèi)容請往下看
ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
} else {
//方法的內(nèi)容請往下看
ReactErrorUtils.invokeGuardedCallback(type, listener, event);
}
event.currentTarget = null;
}
/** ReactErrorUtils.js
* @param {String} name of the guard to use for logging or debugging
* @param {Function} func The function to invoke
* @param {*} a First argument
* @param {*} b Second argument
*/
var caughtError = null;
function invokeGuardedCallback(name, func, a) {
try {
func(a);//直接執(zhí)行回調(diào)方法
} catch (x) {
if (caughtError === null) {
caughtError = x;
}
}
}
var ReactErrorUtils = {
invokeGuardedCallback: invokeGuardedCallback,
invokeGuardedCallbackWithCatch: invokeGuardedCallback,
rethrowCaughtError: function rethrowCaughtError() {
if (caughtError) {
var error = caughtError;
caughtError = null;
throw error;
}
}
};
if (process.env.NODE_ENV !== 'production') {//非生產(chǎn)環(huán)境會通過自定義事件去觸發(fā)回調(diào)
if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof document !== 'undefined' && typeof document.createEvent === 'function') {
var fakeNode = document.createElement('react');
ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
var boundFunc = func.bind(null, a);
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);
};
}
}
復(fù)制代碼

最后react 通過生成了一個臨時節(jié)點fakeNode,然后為這個臨時元素綁定事件處理程序,然后創(chuàng)建自定義事件 Event,通過fakeNode.dispatchEvent方法來觸發(fā)事件,并且觸發(fā)完畢之后立即移除監(jiān)聽事件。
總結(jié)
React的事件系統(tǒng)主要分為3個步驟:
一是事件綁定,ReactBrowserEventEmitter的trapBubbledEvent等方法為節(jié)點或文檔綁定事件;
二是事件監(jiān)聽,ReactEventListener.dispatchEvent將把該回調(diào)函數(shù)分發(fā)給事件對象的_dispatchListener屬性;調(diào)用ReactBrowserEventEmitter.ReactEventListener方法以監(jiān)聽節(jié)點事件。
三是事件分發(fā)與觸發(fā),對觸發(fā)的事件進行分發(fā),并創(chuàng)建合成事件對象,在回調(diào)中用構(gòu)建合成事件對象并執(zhí)行合成事件對的象綁定回調(diào)。
站在巨人肩上
一看就暈的React事件機制
揭秘React形成合成事件的過程
【長文慎入】一文吃透 react 事件機制原理
React 事件系統(tǒng)
React事件機制 - 源碼概覽(上)
react 事件池
React 合成事件和原生事件的區(qū)別
React的事件機制
React event實現(xiàn)原理
為什么需要在 React 類組件中為事件處理程序綁定 this
React合成事件系統(tǒng)
【譯】了解React源代碼-UI更新(DOM樹)9