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在本文場景中,沒毛用
相關文章
相關標籤/搜索