咱們都知道 React 組件綁定事件的本質是代理到 document 上,然而面試被問到,爲何要這麼設計,有什麼好處嗎?html
我知道確定不會是由於虛擬 DOM 的緣由,由於 Vue 的事件就能掛載到真實的 DOM 節點。因此繼續往下探究吧react
設有一段代碼以下git
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>react demo</title> <style> #parent { width: 200px; height: 200px; background-color: black; display: flex; align-items: center; justify-content: center;; } #child { width: 100px; height: 100px; background-color: #FFF; } </style> </head> <body> <div id="app"></div> <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script> <script type="text/babel"> ReactDOM.render( <div id="parent" onClick={() => { console.log('parent!') }}> <div id="child" onClick={() => { console.log('child!') }}></div> </div>, document.getElementById('app') ); </script> </body> </html>
咱們在 child 和 parent 兩個節點都掛上了 onClick 函數,而且點擊 child 觸發事件,的確先輸出 child!後輸出 parent!。此刻大家或許留意到了下圖,瀏覽器的反饋是事件的確只有一個,就是掛在 document 上的。github
這個事件就是 dispatchDiscreteEvent。簡言之,react 本身定了一個 event 對象,存放着 onClick 回調們,在用戶觸發點擊點擊事件時,挨個檢查並執行。面試
咱們都知道事件委託的好處,能夠減小 DOM 上的事件對象節省內存,優化頁面性能。這麼說仍是抽象,舉個例子,如有一 10w 項列表,點擊列表某一項要提示這一列表的某個信息,若你使用 Vue,會在每個 li 節點掛載事件,10w 個事件將會極大程度上拖慢你的瀏覽器性能,你能夠運行下面的例子明顯感到 DOM 加載慢。瀏覽器
<div id="app"> <ul> <li v-for="item in list" @click="handleFn">{{ item }}</li> <ul> </div> let list = []; for (let i = 0; i < 1000000; i++) { list.push(i); } var app = new Vue({ el: '#app', data: { list: list, }, methods: { handleFn() { } } })
解決這個問題的惟一途徑就是事件代理,只須要把事件掛載到 ul 上,並斷定 event.target 來自某個 li。babel
react 掛載到 document 上的行爲天生作了事件代理,省了你這一步操做。app
可是弊端仍是有的,因爲 react 的機制,使得它包裝了一層,開發者無法在冒泡階段拿到原生的事件對象,那麼就提升了學習成本。dom
而且在開發者「不知情」的狀況下埋下了一個坑,若你在 document 上掛載自定義的事件,而且調用了 e.stopImmediatePropagation() 就不會再執行 react 自身綁定在 document 上的事件。見下面的例子ide
<script type="text/babel"> class Toggle extends React.Component { constructor(props) { super(props); document.addEventListener('click', function(e) { console.log('document!'); // 只會輸出 document! ,react 自身的 onClick 回調不會再執行 e.stopImmediatePropagation(); }) } render() { return ( <div id="parent" onClick={() => { console.log('parent!') }}> <div id="child" onClick={() => { console.log('child!') }}></div> </div> ); } } ReactDOM.render( <Toggle />, document.getElementById('app') ); </script>
設上文代碼,點擊了 child 後,只但願 child 的事件被觸發,parent 的不被觸發怎麼作?
結論顯而易見是 stopPropagation。
<div id="child" onClick={(e) => { console.log('child!'); e.stopPropagation() }}></div>
然而上文咱們提到過,react 提供的事件對象是它本身合成的事件對象,它的冒泡是模擬的,它的事件模型應該以下圖,下文圖片出自 github-youngwind -React 事件代理與 stopImmediatePropagation
那麼這個 e.stopPropagation() 是什麼?
貼上了部分源碼,簡單解釋下,react 的合成事件裏刪除了原生事件的 stopPropagation,並本身模擬實現了一個,它標記了一下 this.isPropagationStopped
爲 true,挨個遍歷合成事件對象裏的回調之中,回去檢查這個屬性,爲 true 則不繼續向下執行。
function SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) { { // these have a getter/setter for warnings delete this.nativeEvent; delete this.preventDefault; delete this.stopPropagation; delete this.isDefaultPrevented; delete this.isPropagationStopped; } // ......省略代碼 _assign(SyntheticEvent.prototype, { preventDefault: function() { // ......省略代碼 }, stopPropagation: function () { var event = this.nativeEvent; if (!event) { return; } if (event.stopPropagation) { event.stopPropagation(); } else if (typeof event.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; }, }) // ......省略代碼 }
但其實吧,咱們還有另一種方式能夠組織這種冒泡,就是拿到原生事件對象調用 stopImmediatePropagation,如 e.nativeEvent.stopImmediatePropagation。stopImmediatePropagation 可以阻止掛載到某個 DOM 節點上多個事件的後續執行。下文圖片出自 github-youngwind -React 事件代理與 stopImmediatePropagation
首先 document 上掛載的是 dispatchDiscreteEvent 回調函數
function dispatchDiscreteEvent(topLevelType, eventSystemFlags, container, nativeEvent) { flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp); discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, container, nativeEvent); }
上面函數代理了一堆操做,但總之接下來嘗試分發事件。
function attemptToDispatchEvent(topLevelType, eventSystemFlags, container, nativeEvent) { // TODO: Warn if _enabled is false. var nativeEventTarget = getEventTarget(nativeEvent); // 這個東西就是 react 的虛擬節點 FiberNode {tag: 5, key: null, elementType: "div", type: "div", stateNode: div#child, …} var targetInst = getClosestInstanceFromNode(nativeEventTarget); // ...... 省略判斷觸發節點是否有效性 { dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, targetInst); } // We're not blocked on anything. return null; }
跳過兩步,執行到一個叫 executeDispatchesInOrder 的函數,就要開始按順序的觸發事件。注意函數參數 event 對象,此對象中存放了全部咱們 onClick 預設的回調函數。
function executeDispatchesInOrder(event) { // event._dispatchListeners 其實就是 onClick 的回調函數。 // (2) [ƒ, ƒ] // 0: ƒ onClick(e) // 1: ƒ onClick() var dispatchListeners = event._dispatchListeners; // event._dispatchInstances 其實就是 child 和 parent 的兩個虛擬節點 // (2) [FiberNode, FiberNode] var dispatchInstances = event._dispatchInstances; if (Array.isArray(dispatchListeners)) { // 循環執行回調,除非有 e.stopPropagation() 被觸發,讓 isPropagationStopped 的標記爲 true。 for (var 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; }
那麼真正執行事件並觸發回掉的過程是這樣的,創造了一個叫 react 的假節點,創造了一個事件 evt 並掛到這個節點上,手動觸發它,最後再銷燬。evt 的回掉內容就是咱們的 onClick 裏的內容
{ // ......省略代碼 var fakeNode = document.createElement('react'); // ......省略代碼 var evt = document.createEvent('Event'); // ......省略代碼 function callCallback() { fakeNode.removeEventListener(evtType, callCallback, false); // ......省略代碼 // 注意這個 func 就是咱們的回掉 ƒ onClick() { console.log('child!') } func.apply(context, funcArgs); } // ......省略代碼 var evtType = "react-" + (name ? name : 'invokeguardedcallback'); // Attach our event handlers fakeNode.addEventListener(evtType, callCallback, false); // Synchronously dispatch our fake event. If the user-provided function // errors, it will trigger our global error handler. evt.initEvent(evtType, false, false); fakeNode.dispatchEvent(evt); // ......省略代碼 }
接着循環去執行下一個事件。