React合成事件和DOM原生事件混用须知

引用的React代码,版本均为v15.6.1分支html

React合成事件

为何有合成事件的抽象?

若是DOM上绑定了过多的事件处理函数,整个页面响应以及内存占用可能都会受到影响。React为了不这类DOM事件滥用,同时屏蔽底层不一样浏览器之间的事件系统差别,实现了一个中间层——SyntheticEvent。react

原理

React中,若是须要绑定事件,咱们经常在jsx中这么写:git

<div onClick={this.onClick}>
	react事件
</div>
复制代码

原理大体以下:github

React并非将click事件绑在该div的真实DOM上,而是在document处监听全部支持的事件,当事件发生并冒泡至document处时,React将事件内容封装并交由真正的处理函数运行。浏览器

以上面的代码为例,整个事件生命周期示意以下: dom

其中,因为event对象是复用的,事件处理函数执行完后,属性会被清空,因此event的属性没法被异步访问,详情请查阅event-pooling异步

如何在React中使用原生事件

虽然React封装了几乎全部的原生事件,但诸如:函数

  • Modal开启之后点其余空白区域须要关闭Modal
  • 引入了一些以原生事件实现的第三方库,而且相互之间须要有交互

等等场景时,不得不使用原生事件来进行业务逻辑处理。ui

因为原生事件须要绑定在真实DOM上,因此通常是在componentDidMount阶段/ref的函数执行阶段进行绑定操做,在componentWillUnmount阶段进行解绑操做以免内存泄漏。this

示例以下:

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

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

    render() {
        return (
            <div>Demo</div>
        )
    }
}
复制代码

合成事件和原生事件混合使用

若是业务场景中须要混用合成事件和原生事件,那使用过程当中须要注意以下几点:

响应顺序

先看个简单例子,下面例子中点击Demo之后,控制台输出会是怎样的?

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>
        )
    }
}
复制代码

咱们来分析一下:首先DOM事件监听器被执行,而后事件继续冒泡至document,合成事件监听器再被执行。

即,最终控制台输出为:

dom event react event

阻止冒泡

那,若是在onDOMClick中调用evt.stopPropagation()呢?

因为DOM事件被阻止冒泡了,没法到达document,因此合成事件天然不会被触发,控制台输出就变成了:

dom event

简单例子都比较容易理解,例子再复杂一些:

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>
        )
    }
}
复制代码

若是在onChildClick中调用evt.stopPropagtion(),则控制台输出变为:

child dom event parent dom event child react event

这样的结果是由于React给合成事件封装的stopPropagation函数在调用时给本身加了个isPropagationStopped的标记位来肯定后续监听器是否执行。

源码以下:

// 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])) {
    return dispatchInstances[i];
  }
}
复制代码

nativeEvent在React事件体系中的尴尬位置

有人或许有疑问,虽然响应顺序上合成事件晚于原生事件,那在合成事件中是否能够影响原生事件的监听器执行呢?答案是(几乎)不可能。。。

咱们知道,React事件监听器中得到的入参并非浏览器原生事件,原生事件能够经过evt.nativeEvent来获取。但使人尴尬的是,nativeEvent的做用很是小。

stopPropagation

在使用者的指望中,stopPropagation是用来阻止当前DOM的原生事件冒泡。

但经过上一节合成事件的原理可知,实际上该方法被调用时,实际做用是在DOM最外层阻止冒泡,并不符合预期。

stopImmediatePropagation

stopImmediatePropagation经常在多个第三方库混用时,用来阻止多个事件监听器中的非必要执行。

但React体系中,一个组件只能绑定一个同类型的事件监听器(重复定义时,后面的监听器会覆盖以前的),因此合成事件甚至都不去封装stopImmediatePropagation

事实上nativeEvent的stopImmediatePropagation只能阻止绑定在document上的事件监听器。此外,因为事件绑定的顺序问题,须要注意,若是是在react-dom.js加载前绑定的document事件,stopImmediatePropagation也是没法阻止的。

【冷门】捕获阶段的合成事件

合成事件的文档不少,总得来点新奇的内容。。。

React支持将监听器注册在捕获阶段,但因为应用场景很少,因此基本不太被说起。不过本文涉及到了合成事件,就一并展开下。

将这个例子稍做改造,这时候的控制台输出会怎么样呢?

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>
        )
    }
}
复制代码

结果是:

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

看着好像挺合理,好像又不太合理。或许有人(好比我)会困惑为什么合成事件的捕获阶段响应也晚于原生事件的冒泡阶段响应呢?

实际上是由于,合成事件的代理并非在document上同时注册捕获/冒泡阶段的事件监听器的,事实上只有冒泡阶段的事件监听器,每一次DOM事件的触发,React会在event._dispatchListeners上注入全部须要执行的函数,而后依次循环执行(如上文React源码)。

_dispatchListeners的生成逻辑以下:

// https://github.com/facebook/react/blob/v15.6.1/src/renderers/dom/client/ReactDOMTreeTraversal.js
/* path为react的组件树,由下向上遍历,本例中就是[child, parent]; 而后先将标记为captured的监听器置入_dispatchListeners,此时顺序是path从后往前; 再是标记为bubbled的监听器,顺序是从前日后。 */
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);
  }
}
复制代码

结论

  1. 合成事件的监听器是统一注册在document上的,且仅有冒泡阶段。因此原生事件的监听器响应老是比合成事件的监听器早
  2. 阻止原生事件的冒泡后,会阻止合成事件的监听器执行
  3. 合成事件的nativeEvent在本文场景中,没毛用
相关文章
相关标签/搜索