React事件機制-事件註冊
事件機制
React事件主要分為兩部分: 事件註冊與事件分發。下面先從事件註冊說起。
事件註冊
假設我們的程式如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>React App</title> </head> <body> <div id="root"></div> </body> </html>
import React from 'react'; import ReactDOM from 'react-dom'; class ClickCounter extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } handleClick = () => { this.setState((state) => { return {count: state.count + 1}; }); }; render() { return [ <button key="1" onClick={this.handleClick}>Update counter</button>, <span key="2">{this.state.count}</span>, ] } } ReactDOM.hydrate(<ClickCounter />, document.getElementById('root'));
事件註冊主要發生在初始化Dom屬性的時候,呼叫setInitialProperties
方法,對一些型別dom進行事件繫結。
switch (tag) { case 'iframe': case 'object': trapBubbledEvent(TOP_LOAD, domElement); props = rawProps; break; case 'video': case 'audio': for (var i = 0; i < mediaEventTypes.length; i++) { trapBubbledEvent(mediaEventTypes[i], domElement); } props = rawProps; break; ... } setInitialDOMProperties(tag, domElement, rootContainerElement, props, isCustomComponentTag); ...
接著呼叫setInitialDOMProperties
來真正初始化Dom屬性。根據當前workInProgress
的pendingProps
物件,給Dom物件設定屬性。其中,有個分支會專門處理事件。
// registrationNameModules是一個map物件,儲存著React支援的事件型別 else if (registrationNameModules.hasOwnProperty(propKey)) { if (nextProp != null) { ensureListeningTo(rootContainerElement, propKey); } }
執行ensureListeningTo
方法:
// rootContainerElement為React應用的掛載點, registrationName為onClick function ensureListeningTo(rootContainerElement, registrationName) { // 判斷rootContainerElement是document還是fragment var isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE; // 獲取rootContainerElement所在的document。 var doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument; listenTo(registrationName, doc); }
開始執行listenTo
方法,註冊事件入口。
// 獲取當前已監聽的原生事件型別的map var isListening = getListeningForDocument(mountAt); // 獲取對應的原生事件型別,registrationNameDependencies儲存了React事件型別與瀏覽器原生事件型別對映的一個map var dependencies = registrationNameDependencies[registrationName]; for (var i = 0; i < dependencies.length; i++) { var dependency = dependencies[i]; if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) { switch (dependency) { ...// 除了scroll blur focus cancel close方法調trapCapturedEvent方法,invalid submit reset不處理之外,其餘都調trapBubbledEvent方法。 default: var isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1; if (!isMediaEvent) { trapBubbledEvent(dependency, mountAt); } break; } // 標記該原生事件型別已被註冊,下次註冊同類型事件時會被忽略 isListening[dependency] = true; } }
trapCapturedEvent
與trapBubbledEvent
的區別是前者註冊捕獲階段的事件監聽器,後者註冊冒泡階段的事件監聽器。trapCapturedEvent
使用比較少,所以重點看下trapBubbledEvent
。
//click document function trapBubbledEvent(topLevelType, element) { if (!element) { return null; } // 從字面意能看出,前者是互動類事件,優先順序會比普通事件高(click的分發者是dispatchInteractiveEvent) var dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent; // 註冊事件,在冒泡階段捕獲 addEventBubbleListener(element, getRawEventName(topLevelType), // Check if interactive and wrap in interactiveUpdates dispatch.bind(null, topLevelType)); }
總結
可以發現,React把某一型別事件通過事件代理繫結到document
或fragment
上(fragment
的情況比較少)。即workInProgress
在complete
過程中,如果之前已經註冊過onClick
事件,後續workInProgress
中的onClick
事件將不再註冊,統一由document
中註冊的click
事件代理處理。