最近在閱讀《深刻React技術棧》一書中,發現了以前使用React中並無注意到的React事件與瀏覽器原生事件之間的區別,鑑於很久已經沒有寫東西了,就想寫一下關於React事件的文章。
首先咱們舉個例子,若是咱們須要實現一個組件,這個組件點擊按鈕會顯示一個二維碼,點擊二維碼以外的區域能夠隱藏二維碼,可是點擊二維碼自己卻不會關閉,代碼以下:javascript
//代碼來源於《深刻React技術棧》2.1.4節 class QrCode extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); this.handleClickQr = this.handleClickQr.bind(this); this.state = { active: false, }; } componentDidMount() { document.body.addEventListener('click', e => { this.setState({ active: false, }); }); } componentWillUnmount() { document.body.removeEventListener('click'); } handleClick() { this.setState({ active: !this.state.active, }); } handleClickQr(e) { e.stopPropagation(); } render() { return ( <div className="qr-wrapper"> <button className="qr" onClick={this.handleClick}>二維碼</button> <div className="code" style={{ display: this.state.active ? 'block' : 'none' }} onClick={this.handleClickQr} > <img src="qr.jpg" alt="qr" /> </div> </div> ); } }
上面代碼從感官上感受確實能夠實現要求的組件,但事實上咱們運行上述代碼能夠發現,點擊二維碼自己也會致使二維碼的隱藏,如今就有意思了,咱們來仔細分析一下。
其實React事件並無原生的綁定在真實的DOM上,而是使用了行爲委託方式實現事件機制。
java
如上圖所示,在JavaScript中,事件的觸發實質上是要通過三個階段:事件捕獲、目標對象自己的事件處理和事件冒泡,假設在div
中觸發了click
事件,實際上首先經歷捕獲階段會由父級元素將事件一直傳遞到事件發生的元素,執行完目標事件自己的處理事件後,而後經歷冒泡階段,將事件從子元素向父元素冒泡。正由於事件在DOM的傳遞經歷這樣一個過程,從而爲行爲委託提供了可能。通俗地講,行爲委託的實質就是將子元素事件的處理委託給父級元素處理。React會將全部的事件都綁定在最外層(document
),使用統一的事件監聽,並在冒泡階段處理事件,當掛載或者卸載組件時,只須要在經過的在統一的事件監聽位置增長或者刪除對象,所以能夠提升效率。
而且React並無使用原生的瀏覽器事件,而是在基於Virtual DOM的基礎上實現了合成事件(SyntheticEvent),事件處理程序接收到的是SyntheticEvent的實例。SyntheticEvent徹底符合W3C的標準,所以在事件層次上具備瀏覽器兼容性,與原生的瀏覽器事件同樣擁有一樣的接口,能夠經過stopPropagation()
和preventDefault()
相應的中斷。若是須要訪問當原生的事件對象,能夠經過引用nativeEvent
得到。
上圖爲大體的React事件機制的流程圖,React中的事件機制分爲兩個階段:事件註冊和事件觸發:瀏覽器
事件註冊
React在組件加載(mount
)和更新(update
)時,其中的ReactDOMComponent
會對傳入的事件屬性進行處理,對相關事件進行註冊和存儲。document
中註冊的事件不處理具體的事件,僅對事件進行分發。ReactBrowserEventEmitter
做爲事件註冊入口,擔負着事件註冊和事件觸發。註冊事件的回調函數由EventPluginHub
來統一管理,根據事件的類型(type
)和組件標識(_rootNodeID
)爲key
惟一標識事件並進行存儲。app
事件執行
事件執行時,document上綁定事件ReactEventListener.dispatchEvent
會對事件進行分發,根據以前存儲的類型(type
)和組件標識(_rootNodeID
)找到觸發事件的組件。ReactEventEmitter
利用EventPluginHub
中注入(inject
)的plugins
(例如:SimpleEventPlugin
、EnterLeaveEventPlugin
)會將原生的DOM事件轉化成合成的事件,而後批量執行存儲的回調函,回調函數的執行分爲兩步,第一步是將全部的合成事件放到事件隊列裏面,第二步是逐個執行。須要注意的是,瀏覽器原生會爲每一個事件的每一個listener建立一個事件對象,能夠從這個事件對象獲取到事件的引用。這會形成高額的內存分配,React在啓動時就會爲每種對象分配內存池,用到某一個事件對象時就能夠從這個內存池進行復用,節省內存。函數
再回到咱們剛開始的問題,如今看起來就很沒有很費解了,之因此會出現上面的問題是由於咱們混用了React的事件機制和DOM原生的事件機制,認爲經過:this
handleClickQr(e) { e.stopPropagation(); }
就能阻止原生的事件傳播,其實在事件委託的情形下是不能實現這一點的。固然解決的辦法也不復雜,不要將React事件和DOM原生事件混用。spa
componentDidMount() { document.body.addEventListener('click', e => { this.setState({ active: false, }); }); document.querySelector('.code').addEventListener('click', e => { e.stopPropagation(); }) } componentWillUnmount() { document.body.removeEventListener('click'); document.querySelector('.qr').removeEventListener('click'); }
或者經過事件原件對象中的target
進行判斷:code
componentDidMount() { document.body.addEventListener('click', e => { if (e.target && e.target.matches('div.code')) { return; } this.setState({ active: false, }); }); }
均可以解決異常關閉的問題。component