Note: The conclusions in this article are based on React 16.13.1. If there is any discrepancy, please refer to the corresponding source code

A few questions

Let’s take a look at a few questions first. If you can say the answer with certainty, then you don’t need to read this article.

Click BUTTON to print:

Topic 1:

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>)}}Copy the code

A B C D

Topic 2:

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>)}}Copy the code

A. the B. the C. the D. the

Title 3:

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>)}}Copy the code

A B C

Topic 4:

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>)}}Copy the code

A B C

Did you get them all right?

DOM events

First, let’s briefly review DOM events:

  1. Event delegate. React makes use of event delegates, binding events to documents.
  2. DOM event model. It is divided into capture, target and bubble stages.

Event delegation

As shown below, we want to listen for the click event on the li tag, but instead of binding the event to li, we bind the event to its parent element, using e.target to get the target element for the current click, which is called event delegate. With event delegate we can reduce the number of event listeners on the page and improve performance.

<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>
Copy the code

DOM event model

We know that DOM events are divided into three phases: capture, target, and bubble. Let’s take a few examples to illustrate the workflow:

Example 1:

<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>
Copy the code

The third addEventListener parameter specifies whether to fire the event function during the capture phase. The default is false, so all the above events are fired during the bubble phase. Events are fired from bottom to top, with events on the same element executed in the order they are bound, as shown below:

So the result is:

button click
div click 1
div click 2
div click 3
document click
Copy the code

Example 2:

<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>
Copy the code

E.toppropagation () is added to prevent event propagation, so the event listener function on document will not be executed.

Example 3:

<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>
Copy the code

Here we bind the div’s two event listeners in the capture phase. Listeners for the capture phase are executed first when the event is triggered, from the top down, in the same binding order on the same element.

So the result is:

div click 2
div click 3
button click
div click 1
document click
Copy the code

Example 4:

<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>
Copy the code

Here a new e.s topImmediatePropagation (), the method is enhanced stopPropagation, not only can prevent spread to other elements, can be in this element inside to prevent proliferation.

React Event System

After a review of DOM events, let’s look at how the React event binding works.

React Event binding

First, we know React uses event delegation to bind all events to document (version 17 changed). Specific to the code, you can view the react – the reconciler/SRC/ReactFiberCompleteWork. Old. Js file:

.// Create the real DOM through FiberNode
// The constructor method for the class component has been executed, but not componentDidMount
const instance = createInstance(
  type,
  newProps,
  rootContainerInstance,
  currentHostContext,
  workInProgress
)

...

if (
  // This method will eventually do event binding
  finalizeInitialChildren(
    instance,
    type,
    newProps,
    rootContainerInstance,
    currentHostContext
  )
) {
  ...
}
Copy the code

Including finalizeInitialChildren will call the react – dom/SRC/events/EventListener addEventBubbleListener in js file:

export function addEventBubbleListener(
  target: EventTarget,
  eventType: string,
  listener: Function
) :Function {
  target.addEventListener(eventType, listener, false)
  return listener
}
Copy the code

Note that the constructor function is executed before the event binding, whereas componentDidMount is executed after the event binding.

Events trigger

Let’s use the following example to understand the flow of events:

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>)}}Copy the code

When the event is triggered on the document, we can get the NativeEvent object, the NativeEvent, through the target can access the DOM element button that is currently clicked, Attribute __reactFiber$***** (***** represents a random number) to obtain the FiberNode corresponding to the button.

React also uses NativeEvent to generate SyntheticEvent. SyntheticEvent has several important properties worth paying attention to:

  1. NativeEvent, pointing to theNativeEvent.
  2. _dispatchListeners store event listeners to be executed.
  3. The _dispatchInstances file to which the event listener function to execute belongsFiberNodeObject.

The capture and bubble phases are then used to collect event listeners to execute:

Finally, execute the methods in _dispatchListeners and FiberNode in _dispatchInstances to get 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
}
Copy the code

Attention to the event. The isPropagationStopped (), the method is to check whether the current to prevent spread, suppose we’re on an event monitoring function call e.s topPropagation (), will perform the following code:

function functionThatReturnsTrue() {
  return true; }...stopPropagation: function() {
    const event = this.nativeEvent;
    if(! event) {return;
    }

    if (event.stopPropagation) {
      event.stopPropagation();
    } else if (typeofevent.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; }...Copy the code

In this way, none of the subsequent functions in the _dispatchListeners array will be executed, preventing event proliferation.

Topic answer

Finally, let’s answer the question at the beginning of the passage.

Topic 1:

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>)}}Copy the code

The event model can be simplified to the figure above, where A and B are represented in A box as belonging to different subfunctions of the same event listener. According to the event bubble mechanism, the answer is: D C A B

Topic 2:

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>)}}Copy the code

A) stopPropagation B) stopPropagation C) stopPropagation D) stopPropagation

Title 3:

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>)}}Copy the code

The constructor function precedes the React event binding, so the answer is: C A B

Topic 4:

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>)}}Copy the code

Calling stopImmediatePropagation on native events will stop events from propagating in the element