事件機制 React事件主要分為兩部分: 事件註冊與事件分發。下麵先從事件註冊說起。 事件註冊 假設我們的程式如下: 事件註冊主要發生在初始化Dom屬性的時候,調用 方法,對一些類型dom進行事件綁定。 js switch (tag) { case 'iframe': case 'object': ...
事件機制
本系列以React v16.8.3為基礎進行源碼分析
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
事件代理處理。