官方文檔對React事件的介紹包含如下幾點css
那麼在看源碼以前,有如下疑問:html
React 版本號 16.9.0
瞭解源碼最好的方式是單步調試,找一個最簡單的例子,在源碼中打斷點進行調試。本文采用create-react-app建立了最簡單的demo,只含有click事件。頁面內容以下react
import React from 'react'; import './App.css'; class App extends React.Component { spanClickEvent = null; headerClickEvent = null; componentDidMount () { // document.addEventListener('click', () => { // console.log('document click'); // }) } spanClick (event) { event.stopPropagation(); console.log('spanClick'); console.log(event); // this.spanClickEvent = event; } headerClick (event) { console.log('headerClick'); console.log(event); // this.headerClickEvent = event; // console.log(this.headerClickEvent === this.spanClickEvent); } inputChange (event) { console.log('inputChange'); console.log(event); } render () { return ( <div className="App"> <header className="App-header" onClick={(event) => this.headerClick(event)}> <div className="btn-wrapper"> <span className="btn" onClick={(event) => this.spanClick(event)}> <span>點擊</span> </span> {/* <input onChange={(event) => this.inputChange(event)}/> */} </div> </header> </div> )}; } export default App;
首先刷新頁面,在render過程當中會走到以下邏輯瀏覽器
function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) { var parentNamespace = void 0; { // 此處省略代碼... } var domElement = createElement(type, props, rootContainerInstance, parentNamespace); // 將internalInstanceHandle和props掛載在真實的DOM上,後面會用到 precacheFiberNode(internalInstanceHandle, domElement); updateFiberProps(domElement, props); return domElement; }
updateFiberProps會走到setInitialDOMProperties裏面app
function setInitialDOMProperties(tag, domElement, rootContainerElement, nextProps, isCustomComponentTag) { // 此處省略代碼 else if (registrationNameModules.hasOwnProperty(propKey)) { if (nextProp != null) { if (true && typeof nextProp !== 'function') { warnForInvalidEventListener(propKey, nextProp); } // 若是props含有事件相關的屬性,則去監聽對應的事件 ensureListeningTo(rootContainerElement, propKey); } } }
注意此處使用的registrationNameModules存放了全部React事件。dom
ensureListeningTo會判斷當前是否在iframe裏面,以決定監聽哪裏的事件。最後走到listenTo邏輯裏面。異步
function listenTo(registrationName, mountAt) { //listeningSet存放了已經監聽過的事件,避免重複去監聽。 var listeningSet = getListeningSetForElement(mountAt); var dependencies = registrationNameDependencies[registrationName]; for (var i = 0; i < dependencies.length; i++) { var dependency = dependencies[i]; if (!listeningSet.has(dependency)) { // 初始化span標籤的時候會走到這個邏輯裏面,header的時候就不會再重複去監聽click了 switch (dependency) { // 此處省略代碼 default: var isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1; if (!isMediaEvent) { trapBubbledEvent(dependency, mountAt); } break; } listeningSet.add(dependency); } } }
registrationNameDependencies存放了React事件與原生事件須要監聽的對應關係。以下圖中,若是使用onBlur則會監聽window的blur事件,若是使用onChange則會監聽blur/change/..等事件this
接下來講trapBubbledEventspa
function addEventBubbleListener(element, eventType, listener) { // 注意此處element是document,第三個參數是false element.addEventListener(eventType, listener, false); }
此時的listener爲dispatchDiscreteEvent調試
至此,事件註冊完成。值得注意的是,React在生成的真實DOM中加入了兩個React屬性,一個放了元素的props,一個放了元素對應的FiberNode。
原生DOM和FiberNode的一個雙向關係。
傳中...]
點擊span元素後,會走到dispatchDiscreteEvent邏輯裏面,會帶着nativeEvent調dispatchEvent方法。
經過getEventTarget(nativeEvent)
拿到當前的nativeEvent.target
爲<span>點擊元素</span>
,而後拿到DOM上含有__reactInternalInstance*的最近的元素,此處爲<span>點擊元素</span>
。調用dispatchEventForPluginEventSystem,調用batchedEventUpdates,中間會調用runExtractedPluginEventsInBatch處理原生事件,將原生事件合成爲合成事件。最後會調用到traverseTwoPhase。這個方法主要是找到當前的path鏈
溫習currentTarget和target currentTarget表示事件處理程序當前正在處理事件的那個元素
target 事件的目標
function traverseTwoPhase(inst, fn, arg) { var 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); } }
調用對應的fn也就是accumulateDirectionalDispatches
function accumulateDirectionalDispatches(inst, phase, event) { // 省略代碼 // 在'bubble'階段的onClick對應onClick,而captured的onClick對應onClickCaptured。所以咱們在捕獲階段沒有事件能夠觸發。感興趣的能夠將demo中的onClick更改成onClickCaptured模擬捕獲觸發 var listener = listenerAtPhase(inst, event, phase); if (listener) { // 依次拿到span.btn和header上的onClick,而且放進event._dispatchListeners event._dispatchListeners = accumulateInto(event._dispatchListeners, listener); event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); } }
最後調用executeDispatchesInOrder,遍歷_dispatchListeners依次觸發。觸發的時候會判斷event.isPropagationStopped()是true仍是false
function executeDispatch(event, listener, inst) { var type = event.type || 'unknown-event'; // 賦值給currentTarget event.currentTarget = getNodeFromInstance(inst); invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event); event.currentTarget = null; }
最後調用到fakeNode.dispatchEvent觸發callCallback真正的onClick事件。此處採用fakeNode.dispatchEvent是爲了讓事件仍然是瀏覽器發起的。
調用完畢後,會將event初始化爲最初的狀態
整個過程,能夠發現如下問題
回到最初的問題提問
例子中的span調用了stopPropagation,那麼如下代碼會觸發嗎
componentDidMount () { document.addEventListener('click', () => { // 依然會觸發。爲何? console.log('document click'); }) window.addEventListener('click', () => { // 不會觸發。爲何? console.log('document click'); }) }
另外一個問題,React是何時removeEventListner的?目前的出來的結論是並無。以下例子,在點擊header時會觸發React的DispatchEvent沒有問題。可是在isShow爲false後,點擊span元素,仍然會觸發DispatchEvent。所以目前的結論是,React並無去移除無用的EventListner。這個問題歡迎在評論區交流
class App extends React.Component { constructor () { super(); this.state = { isShow: true }; } headerClick (event) { this.setState({ isShow: false }); } render () { return ( <div className="App"> { this.state.isShow ? <header className="App-header" onClick={(event) => this.headerClick(event)}> </header> : <span>點擊</span> } </div> )}; }