經過例子來理解 React 的事件系統

說明:本文結論均基於 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 事件

首先,咱們先簡單地複習下 DOM 事件的相關知識點:

  1. 事件委託。React 利用了事件委託,將事件都綁定在 document 之上。
  2. 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 事件模型

咱們知道 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,不只能夠阻止向其餘元素擴散,也能夠在本元素內部阻止擴散。

React 事件系統

回顧了下 DOM 事件的知識點後咱們進入正題,首先咱們看 React 事件綁定是怎麼作的。

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 有幾個重要的屬性值得關注下:

  1. nativeEvent,指向 NativeEvent
  2. _dispatchListeners,存儲要執行的事件監聽函數。
  3. _dispatchInstances,存儲要執行的事件監聽函數所屬的 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

相關文章
相關標籤/搜索