官方文檔對React事件的介紹包含如下幾點css
那麼在看源碼以前,有如下疑問:html
React 版本號 16.9.0react
瞭解源碼最好的方式是單步調試,找一個最簡單的例子,在源碼中打斷點進行調試。本文采用create-react-app建立了最簡單的demo,只含有click事件。頁面內容以下瀏覽器
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過程當中會走到以下邏輯bash
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/..等事件ui
接下來講trapBubbledEventthis
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>
)};
}
複製代碼