說明:本文結論均基於 React 16.13.1 得出,如有出入請參考對應版本源碼javascript
咱們先來看幾個題目,若是你都能很肯定的說出結果,那麼這篇文章就不用看了。html
點擊 BUTTON
打印的結果是:java
題目一:react
export default class App extends React.Component {
innerClick = () => {
console.log('A: react inner click.')
}
outerClick = () => {
console.log('B: react outer click.')
}
componentDidMount() {
document.getElementById('outer').addEventListener('click', () => {
console.log('C: native outer click')
})
document.getElementById('inner').addEventListener('click', () => {
console.log('D: native inner click')
})
}
render() {
return (
<div id='outer' onClick={this.outerClick}> <button id='inner' onClick={this.innerClick}> BUTTON </button> </div>
)
}
}
複製代碼
答案:D C A B數組
題目二:markdown
export default class App extends React.Component {
innerClick = (e) => {
console.log('A: react inner click.')
e.stopPropagation()
}
outerClick = () => {
console.log('B: react outer click.')
}
componentDidMount() {
document.getElementById('outer').addEventListener('click', () => {
console.log('C: native outer click')
})
document.getElementById('inner').addEventListener('click', () => {
console.log('D: native inner click')
})
}
render() {
return (
<div id='outer' onClick={this.outerClick}> <button id='inner' onClick={this.innerClick}> BUTTON </button> </div>
)
}
}
複製代碼
答案:D C Adom
題目三:函數
export default class extends React.Component {
constructor(props) {
super(props)
document.addEventListener('click', () => {
console.log('C: native document click')
})
}
innerClick = () => {
console.log('A: react inner click.')
}
outerClick = () => {
console.log('B: react outer click.')
}
render() {
return (
<div id='outer' onClick={this.outerClick}> <button id='inner' onClick={this.innerClick}> BUTTON </button> </div>
)
}
}
複製代碼
答案:C A B性能
題目四:ui
export default class extends React.Component {
constructor(props) {
super(props)
document.addEventListener('click', () => {
console.log('C: native document click')
})
}
innerClick = (e) => {
console.log('A: react inner click.')
e.nativeEvent.stopImmediatePropagation()
}
outerClick = () => {
console.log('B: react outer click.')
}
componentDidMount() {
document.addEventListener('click', () => {
console.log('D: native document click')
})
}
render() {
return (
<div id='outer' onClick={this.outerClick}> <button id='inner' onClick={this.innerClick}> BUTTON </button> </div>
)
}
}
複製代碼
答案:C A B
你全都答對了嗎?
首先,咱們先簡單地複習下 DOM 事件的相關知識點:
以下所示,咱們想監聽 li
標籤上的點擊事件,可是咱們不把事件綁定在 li
上,而是綁定在它的父元素上,經過 e.target
來獲取當前點擊的目標元素,這種作法就是事件委託。經過事件委託咱們能夠減小頁面中的事件監聽函數,提高性能。
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script> const $ul = document.querySelector('ul') $ul.addEventListener('click', (e) => { console.log(e.target.innerText) }) </script>
複製代碼
咱們知道 DOM 事件分爲三個階段:捕獲、目標、冒泡。咱們經過幾個例子來講明其工做流程:
例一:
<div id="id">
<button id="btn">Button</button>
</div>
<script> const $div = document.querySelector('#id') const $btn = document.querySelector('#btn') document.addEventListener('click', () => { console.log('document click') }) $div.addEventListener('click', (e) => { console.log('div click 1') }) $div.addEventListener('click', (e) => { console.log('div click 2') }) $div.addEventListener('click', (e) => { console.log('div click 3') }) $btn.addEventListener('click', () => { console.log('button click') }) </script>
複製代碼
咱們知道, addEventListener
第三個參數是指定是否在捕獲階段觸發事件相應函數,默認 false
,因此上面的事件均在冒泡階段觸發。事件觸發的順序是從下至上,同一個元素上的事件按照綁定的順序執行,以下圖:
因此結果是:
button click
div click 1
div click 2
div click 3
document click
複製代碼
例二:
<div id="id">
<button id="btn">Button</button>
</div>
<script> const $div = document.querySelector('#id') const $btn = document.querySelector('#btn') document.addEventListener('click', () => { console.log('document click') }) $div.addEventListener('click', (e) => { console.log('div click 1') }) $div.addEventListener('click', (e) => { e.stopPropagation() console.log('div click 2') }) $div.addEventListener('click', (e) => { console.log('div click 3') }) $btn.addEventListener('click', () => { console.log('button click') }) </script>
複製代碼
這裏新加了一句 e.stopPropagation()
,其做用是阻止事件擴散,因此 document
上的事件監聽函數就不會執行了。
例三:
<div id="id">
<button id="btn">Button</button>
</div>
<script> const $div = document.querySelector('#id') const $btn = document.querySelector('#btn') document.addEventListener('click', () => { console.log('document click') }) $div.addEventListener('click', (e) => { console.log('div click 1') }) $div.addEventListener( 'click', (e) => { console.log('div click 2') }, true ) $div.addEventListener( 'click', (e) => { console.log('div click 3') }, true ) $btn.addEventListener('click', () => { console.log('button click') }) </script>
複製代碼
這裏把 div
的兩個事件監聽函數綁定在捕獲階段。當事件觸發的時候會先執行捕獲階段的監聽函數,執行順序是從上而下,相同元素上仍然按照綁定順序執行。
因此結果是:
div click 2
div click 3
button click
div click 1
document click
複製代碼
例四:
<div id="id">
<button id="btn">Button</button>
</div>
<script> const $div = document.querySelector('#id') const $btn = document.querySelector('#btn') document.addEventListener('click', () => { console.log('document click') }) $div.addEventListener('click', (e) => { console.log('div click 1') }) $div.addEventListener( 'click', (e) => { e.stopImmediatePropagation() console.log('div click 2') }, true ) $div.addEventListener( 'click', () => { console.log('div click 3') }, true ) $btn.addEventListener('click', () => { console.log('button click') }) </script>
複製代碼
這裏新增了 e.stopImmediatePropagation()
,該方法是增強版的 stopPropagation
,不只能夠阻止向其餘元素擴散,也能夠在本元素內部阻止擴散。
回顧了下 DOM 事件的知識點後咱們進入正題,首先咱們看 React 事件綁定是怎麼作的。
首先,咱們知道 React 利用了事件委託機制,將全部事件綁定到了 document
之上(17 版本有變更)。 具體到代碼,能夠查看 react-reconciler/src/ReactFiberCompleteWork.old.js
文件:
...
// 經過 FiberNode 建立真實 DOM
// 這裏已經執行過類組件的 constructor 方法,可是尚未執行 componentDidMount
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress
)
...
if (
// 該方法最終會進行事件綁定
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext
)
) {
...
}
複製代碼
其中 finalizeInitialChildren
最終會調用 react-dom/src/events/EventListener.js
文件中的 addEventBubbleListener
:
export function addEventBubbleListener( target: EventTarget, eventType: string, listener: Function ): Function {
target.addEventListener(eventType, listener, false)
return listener
}
複製代碼
注意, constructor
函數在事件綁定前就執行了,而 componentDidMount
則在事件綁定以後才執行。
咱們用下面的例子來體會事件觸發的流程:
export default class App extends React.Component {
innerClick = () => {
console.log('A: react inner click.')
}
outerClick = () => {
console.log('B: react outer click.')
}
render() {
return (
<div id='outer' onClickCapture={this.outerClick}> <button id='inner' onClick={this.innerClick}> BUTTON </button> </div>
)
}
}
複製代碼
當事件在 document
上觸發的時候,咱們能夠拿到原生事件對象 NativeEvent
,經過 target
能夠訪問到當前點擊的 DOM
元素 button
,經過其屬性 __reactFiber$*****
(*****表示隨機數)能夠獲取 button
所對應的 FiberNode
。
同時,React 還會利用 NativeEvent
來生成 SyntheticEvent
,其中 SyntheticEvent
有幾個重要的屬性值得關注下:
NativeEvent
。FiberNode
對象。接下來就會分捕獲和冒泡兩個階段來收集要執行的事件監聽函數:
最後,按照順序執行 _dispatchListeners
中的方法,並經過 _dispatchInstances
中的 FiberNode
來獲得 currentTarget
。
export function executeDispatch(event, listener, inst) {
const type = event.type || 'unknown-event'
event.currentTarget = getNodeFromInstance(inst)
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event)
event.currentTarget = null
}
/** * Standard/simple iteration through an event's collected dispatches. */
export function executeDispatchesInOrder(event) {
const dispatchListeners = event._dispatchListeners
const dispatchInstances = event._dispatchInstances
if (__DEV__) {
validateEventDispatches(event)
}
if (Array.isArray(dispatchListeners)) {
for (let i = 0; i < dispatchListeners.length; i++) {
if (event.isPropagationStopped()) {
break
}
// Listeners and Instances are two parallel arrays that are always in sync.
executeDispatch(event, dispatchListeners[i], dispatchInstances[i])
}
} else if (dispatchListeners) {
executeDispatch(event, dispatchListeners, dispatchInstances)
}
event._dispatchListeners = null
event._dispatchInstances = null
}
複製代碼
注意到 event.isPropagationStopped()
,該方法是檢查當前是否要阻止擴散,假設咱們在某個事件監聽函數中調用 e.stopPropagation()
,則會執行下面的代碼:
function functionThatReturnsTrue() {
return true;
}
...
stopPropagation: function() {
const event = this.nativeEvent;
if (!event) {
return;
}
if (event.stopPropagation) {
event.stopPropagation();
} else if (typeof event.cancelBubble !== 'unknown') {
// The ChangeEventPlugin registers a "propertychange" event for
// IE. This event does not support bubbling or cancelling, and
// any references to cancelBubble throw "Member not found". A
// typeof check of "unknown" circumvents this issue (and is also
// IE specific).
event.cancelBubble = true;
}
this.isPropagationStopped = functionThatReturnsTrue;
}
...
複製代碼
這樣,_dispatchListeners
數組中後面的函數就都不會執行了,從而實現了阻止事件擴散的功能。
最後,讓咱們來對文章開頭的題目作一個解答。
題目一:
export default class App extends React.Component {
innerClick = () => {
console.log('A: react inner click.')
}
outerClick = (e) => {
console.log('B: react outer click.')
}
componentDidMount() {
document.getElementById('outer').addEventListener('click', () => {
console.log('C: native outer click')
})
document.getElementById('inner').addEventListener('click', () => {
console.log('D: native inner click')
})
}
render() {
return (
<div id='outer' onClick={this.outerClick}> <button id='inner' onClick={this.innerClick}> BUTTON </button> </div>
)
}
}
複製代碼
事件模型能夠簡化爲上圖,其中 A B 在一個框中表示他們屬於同一個事件監聽函數中的不一樣子函數。根據事件冒泡機制,答案爲:D C A B
題目二:
export default class App extends React.Component {
innerClick = () => {
console.log('A: react inner click.')
e.stopPropagation()
}
outerClick = (e) => {
console.log('B: react outer click.')
}
componentDidMount() {
document.getElementById('outer').addEventListener('click', () => {
console.log('C: native outer click')
})
document.getElementById('inner').addEventListener('click', () => {
console.log('D: native inner click')
})
}
render() {
return (
<div id='outer' onClick={this.outerClick}> <button id='inner' onClick={this.innerClick}> BUTTON </button> </div>
)
}
}
複製代碼
調用了 stopPropagation
,因此 B 不打印,答案爲:D C A
題目三:
export default class extends React.Component {
constructor(props) {
super(props)
document.addEventListener('click', () => {
console.log('C: native document click')
})
}
innerClick = (e) => {
console.log('A: react inner click.')
}
outerClick = () => {
console.log('B: react outer click.')
}
render() {
return (
<div id='outer' onClick={this.outerClick}> <button id='inner' onClick={this.innerClick}> BUTTON </button> </div>
)
}
}
複製代碼
constructor
函數先於 React 事件綁定,因此答案爲:C A B
題目四:
export default class extends React.Component {
constructor(props) {
super(props)
document.addEventListener('click', () => {
console.log('C: native document click')
})
}
innerClick = (e) => {
console.log('A: react inner click.')
e.nativeEvent.stopImmediatePropagation()
}
outerClick = () => {
console.log('B: react outer click.')
}
componentDidMount() {
document.addEventListener('click', () => {
console.log('D: native document click')
})
}
render() {
return (
<div id='outer' onClick={this.outerClick}> <button id='inner' onClick={this.innerClick}> BUTTON </button> </div>
)
}
}
複製代碼
調用原生事件上的 stopImmediatePropagation
,會阻止事件在本元素中繼續擴散,因此答案爲:C A B