The React code referenced is a v15.6.1 branch

React synthesis event

Why is there an abstraction of synthetic events?

If too many event handlers are bound to the DOM, the overall page response and memory footprint can suffer. React implements an intermediate layer, SyntheticEvent, to avoid this kind of DOM event abuse and to mask the event system differences between the underlying browsers.

The principle of

In React, if we need to bind events, we often write this in JSX:

<div onClick={this.onClick}>The react event</div>
Copy the code

The principle is roughly as follows:

Instead of tying the click event to the actual DOM of that div, React listens for all supported events at document. When the event happens and bubbles up to document, React wraps the event content and hands it off to the actual handler.

As an example, the entire event life cycle is shown as follows:

The event objects are reused and the attributes will be emptied after the event handler is executed. Therefore, the attributes of events cannot be accessed asynchronously. For details, see event-pooling.

How to use native events in React

React encapsulates almost all native events, but for example:

  • After Modal is enabled, you need to disable Modal in other blank areas
  • Some third-party libraries have been introduced that are implemented as native events and need to interact with each other

And so on, you have to use native events for business logic processing.

Since native events need to be bound to the real DOM, they are typically bound at componentDidMount stage /ref’s function execution stage and unbound at componentWillUnmount stage to avoid memory leaks.

The following is an example:

class Demo extends React.PureComponent {
    componentDidMount() {
        const $this = ReactDOM.findDOMNode(this)
        $this.addEventListener('click'.this.onDOMClick, false)
    }

    onDOMClick = evt= > {
        // ...
    }

    render() {
        return (
            <div>Demo</div>)}}Copy the code

Use a mix of composite events and native events

If a business scenario requires a mix of synthesized and native events, the following points need to be noted:

In response to the order

Let’s start with a simple example. What would the console output look like if I clicked Demo in the following example?

class Demo extends React.PureComponent {
    componentDidMount() {
        const $this = ReactDOM.findDOMNode(this)
        $this.addEventListener('click'.this.onDOMClick, false)
    }

    onDOMClick = evt= > {
        console.log('dom event')
    }
    
    onClick = evt= > {
        console.log('react event')
    }

    render() {
        return (
            <div onClick={this.onClick}>Demo</div>)}}Copy the code

Let’s look at this: first the DOM event listener is executed, then the event continues to bubble up to the Document, and then the synthesized event listener is executed.

That is, the final console output is:

dom event react event

To prevent a bubble

What if evt.stopPropagation() is called in onDOMClick?

Since the DOM event is prevented from bubbling and cannot reach the Document, the composite event is naturally not fired, and the console output becomes:

dom event

Simple examples are easier to understand, but more complex examples are:

class Demo extends React.PureComponent {
    componentDidMount() {
        const $parent = ReactDOM.findDOMNode(this)
        const $child = $parent.querySelector('.child')
        
        $parent.addEventListener('click'.this.onParentDOMClick, false)
        $child.addEventListener('click'.this.onChildDOMClick, false)
    }

    onParentDOMClick = evt= > {
        console.log('parent dom event')
    }
    
    onChildDOMClick = evt= > {
        console.log('child dom event')
    }    
    
    onParentClick = evt= > {
        console.log('parent react event')
    }

    onChildClick = evt= > {
        console.log('child react event')
    }

    render() {
        return (
            <div onClick={this.onParentClick}>
                <div className="child" onClick={this.onChildClick}>
                    Demo
                </div>
            </div>)}}Copy the code

If evt.stoption () is called in onChildClick, the console output becomes:

child dom event parent dom event child react event

The result is that the stopPropagation function encapsulated by React gives itself an isPropagationStopped flag to determine whether subsequent listeners are executed.

The source code is as follows:

/ / https://github.com/facebook/react/blob/v15.6.1/src/renderers/shared/stack/event/EventPluginUtils.js
for (var i = 0; i < dispatchListeners.length; i++) {
  if (event.isPropagationStopped()) {
    break;
  }
  // Listeners and Instances are two parallel arrays that are always in sync.
  if (dispatchListeners[i](event, dispatchInstances[i])) {
    returndispatchInstances[i]; }}Copy the code

The awkward position of nativeEvent in the React event system

One might wonder, although the response sequence of the synthesized event is later than that of the native event, can the listener execution of the native event be affected in the synthesized event? The answer is (almost) impossible…

As we know, the React event listener gets input parameters that are not browser native events. Native events can be obtained via evt.nativeEvent. But embarrassingly, the role of nativeEvent is very small.

stopPropagation

In the expectation of the user, stopPropagation is used to prevent native events bubblingin the current DOM.

However, as you can see from the compositing event principle in the previous section, this method is actually called to prevent bubbling in the outermost layer of the DOM, which is not as expected.

stopImmediatePropagation

StopImmediatePropagation is often used to prevent unnecessary execution in multiple event listeners when multiple third-party libraries are mixed.

However, in the React system, a component can only be bound to one event listener of the same type (when repeated definitions are made, subsequent listeners will overwrite the previous ones), so the synthesized events do not even encapsulate stopImmediatePropagation.

In fact, the stopImmediatePropagation of nativeEvent only prevents event listeners bound to document. In addition, due to the sequence of event binding, note that if the Document event is bound before the react-dom.js loading, stopImmediatePropagation cannot be blocked.

Capture phase compositing events

There’s a lot of documentation for compositing events, so something new is needed…

React supports registering listeners in the capture phase, but is rarely mentioned due to its limited application scenarios. However, this article deals with composite events, so let’s do it together.

Changing this example a bit, what would the console output look like?

class Demo extends React.PureComponent {
    componentDidMount() {
        const $parent = ReactDOM.findDOMNode(this)
        const $child = $parent.querySelector('.child')
        
        $parent.addEventListener('click'.this.onParentDOMClick, true)
        $child.addEventListener('click'.this.onChildDOMClick, false)
    }

    onParentDOMClick = evt= > {
        console.log('captrue: parent dom event')
    }
    
    onChildDOMClick = evt= > {
        console.log('bubble: child dom event')
    }    
    
    onParentClick = evt= > {
        console.log('capture: parent react event')
    }

    onChildClick = evt= > {
        console.log('bubble: child react event')
    }

    render() {
        return (
            <div onClickCapture={this.onParentClick}>
                <div className="child" onClick={this.onChildClick}>
                    Demo
                </div>
            </div>)}}Copy the code

The result is:

captrue: parent dom event bubble: child dom event capture: parent react event bubble: child react event

It seems reasonable, but it doesn’t seem reasonable. Some people (like me) might wonder why the capture phase response of a composite event is also later than the bubble phase response of a native event.

Note that the agent does not register the capture/bubbling listeners on the Document, but only bubbling listeners. Every time a DOM event is triggered, React will inject all functions on the event._dispatchlisteners. Then execute it in a loop (see the React source code above).

The logic of _dispatchListeners is as follows:

/ / https://github.com/facebook/react/blob/v15.6.1/src/renderers/dom/client/ReactDOMTreeTraversal.js
/* Path is the react component tree, traversed from bottom to top, in this case [child, parent]; Listeners that are marked down are placed _dispatchListeners first, in the order of path from end to end. And then there's the bubbled listener, from front to back. * /
function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  while (inst) {
    path.push(inst);
    inst = inst._hostParent;
  }
  var i;
  for (i = path.length; i-- > 0;) { fn(path[i],'captured', arg);
  }
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg); }}Copy the code

conclusion

  1. Listeners for synthesized events are uniformly registered on the Document and have only the bubbling phase. So listeners for native events always respond earlier than listeners for synthetic events
  2. Preventing bubbling of native events prevents listeners of synthesized events from executing
  3. The nativeEvent of the synthesized event is useless in this scenario