1、原生Dom的事件流

如上圖所示:
在JavaScript中,事件的觸發(fā)實(shí)質(zhì)上是要經(jīng)過(guò)三個(gè)階段:事件捕獲、目標(biāo)對(duì)象本身的事件處理和事件冒泡。
事件捕獲 (由父級(jí)元素將事件一直傳遞到事件發(fā)生的元素)
當(dāng)某個(gè)事件觸發(fā)時(shí),文檔根節(jié)點(diǎn)最先接受到事件,然后根據(jù)DOM樹結(jié)構(gòu)向具體綁定事件的元素傳遞。該階段為父元素截獲事件提供了機(jī)會(huì)。
事件傳遞路徑為:
window —> document —> body —> div—> text目標(biāo)階段
具體元素已經(jīng)捕獲事件后,執(zhí)行目標(biāo)事件本身的處理事件。事件冒泡
根據(jù)DOM樹結(jié)構(gòu)由具體觸發(fā)事件的元素向根節(jié)點(diǎn)傳遞。正因?yàn)槭录贒OM的傳遞經(jīng)歷這樣一個(gè)過(guò)程,從而為事件委托提供了可能。
事件傳遞路徑:
text—> div —> body —> document —> window
2、事件委托(事件代理)
事件委托的實(shí)質(zhì)就是將子元素事件的處理委托給父級(jí)元素處理。把事件監(jiān)聽(tīng)器添加到它們的父元素上。事件監(jiān)聽(tīng)器會(huì)分析從子元素冒泡上來(lái)的事件,找到它是哪個(gè)子元素的事件。
舉個(gè)栗子:如果我們有一個(gè)列表,列表之中有大量的列表項(xiàng),我們需要在點(diǎn)擊列表項(xiàng)的時(shí)候響應(yīng)一個(gè)事件
<body>
<div class="lists">
<div class="list-item">content1</div>
<div class="list-item">content2</div>
<div class="list-item">content3</div>
<div class="list-item">content4</div>
</div>
<script>
//1. 事件代理
// var listParentNode = document.querySelector('.lists');
// listParentNode.addEventListener('click', function(e){
// console.log(e.target);
// })
//新增一個(gè)子元素
// var newItem = document.createElement('div')
// newItem.innerHTML = 'newConent';
// newItem.setAttribute('class', 'list-item');
// listParentNode.appendChild(newItem)
//2. 給每個(gè)列表項(xiàng)一一綁定一個(gè)函數(shù)
var nodeLists = document.querySelectorAll('.list-item');
for(var i = 0; i < nodeLists.length; i++) {
nodeLists[i].addEventListener('click', function(e){
console.log(e.target)
})
}
//新增一個(gè)子元素
var listParentNode = document.querySelector('.lists');
var newItem = document.createElement('div')
newItem.innerHTML = 'newConent';
newItem.setAttribute('class', 'list-item');
listParentNode.appendChild(newItem)
</body>
事件委托優(yōu)點(diǎn):
減少內(nèi)存的消耗
上面兩種方式, 如果給每個(gè)列表項(xiàng)一一都綁定一個(gè)函數(shù),那對(duì)于內(nèi)存消耗是非常大的,效率上需要消耗很多性能;所以比較好的方法是 點(diǎn)擊事件綁定到他的父層,然后在執(zhí)行事件的時(shí)候再去匹配判斷目標(biāo)元素(這樣用事件代理的方式綁定事件,新添加的子元素仍然可以執(zhí)行事件回調(diào)函數(shù))。動(dòng)態(tài)綁定事件
比如上述的例子中列表項(xiàng)就幾個(gè),我們給每個(gè)列表項(xiàng)都綁定了事件;
在很多時(shí)候,我們需要通過(guò) AJAX 或者用戶操作動(dòng)態(tài)的增加或者去除列表項(xiàng)元素,那么在每一次改變的時(shí)候都需要重新給新增的元素綁定事件,給即將刪去的元素解綁事件;
如果用了事件委托就沒(méi)有這種麻煩了,因?yàn)槭录墙壎ㄔ诟笇拥?,和目?biāo)元素的增減是沒(méi)有關(guān)系的,執(zhí)行到目標(biāo)元素是在真正響應(yīng)執(zhí)行事件函數(shù)的過(guò)程中去匹配的;
所以使用事件在動(dòng)態(tài)綁定事件的情況下是可以減少很多重復(fù)工作的。
不適用的情況: 比如 focus、blur 之類的事件本身沒(méi)有事件冒泡機(jī)制,所以無(wú)法委托; mousemove、mouseout這樣的事件,雖然有事件冒泡,但是只能不斷通過(guò)位置去計(jì)算定位,對(duì)性能消耗高,因此也是不適合于事件委托。
3. React事件機(jī)制

在 React事件介紹 中介紹了合成事件對(duì)象以及為什么提供合成事件對(duì)象,主要原因是因?yàn)?React 想實(shí)現(xiàn)一個(gè)全瀏覽器的框架, 為了實(shí)現(xiàn)這種目標(biāo)就需要提供全瀏覽器一致性的事件系統(tǒng),以此抹平不同瀏覽器的差異。
合成事件對(duì)象很有意思,一開(kāi)始聽(tīng)名字會(huì)覺(jué)得很奇怪,看到英文名更奇怪 SyntheticEvent, 實(shí)際上合成事件的意思就是使用原生事件合成一個(gè) React 事件, 例如使用原生click事件合成了onClick事件,使用原生mouseout事件合成了onMouseLeave事件,原生事件和合成事件類型大部分都是一一對(duì)應(yīng),只有涉及到兼容性問(wèn)題時(shí)我們才需要使用不對(duì)應(yīng)的事件合成。
合成事件是瀏覽器的原生事件的跨瀏覽器包裝器。除兼容所有瀏覽器外,它還擁有和瀏覽器原生事件相同的接口,包括 stopPropagation() 和 preventDefault()。
當(dāng)我們?cè)诮M件上設(shè)置事件處理器時(shí),React并不會(huì)在該DOM元素上直接綁定事件處理器. React內(nèi)部自定義了一套事件系統(tǒng),在這個(gè)系統(tǒng)上統(tǒng)一進(jìn)行事件訂閱和分發(fā)。
具體來(lái)講,React利用事件委托機(jī)制在Document上統(tǒng)一監(jiān)聽(tīng)DOM事件,再根據(jù)觸發(fā)的target將事件分發(fā)到具體的組件實(shí)例。另外上面e是一個(gè)合成事件對(duì)象(SyntheticEvent), 而不是原始的DOM事件對(duì)象。
React事件系統(tǒng)實(shí)現(xiàn)可以分為兩個(gè)階段:事件注冊(cè)、事件觸發(fā)
- 事件注冊(cè)
React 的事件注冊(cè)過(guò)程主要做了兩件事:document 上注冊(cè)、存儲(chǔ)事件回調(diào)。
document 上注冊(cè)
React在組件加載(mount)和更新(update)時(shí), 根據(jù)傳入組件內(nèi)的聲明的事件類型的屬性(onClick、onChange 等),在 document 上注冊(cè)事件(document.addEventListener('click', dispatchEvent)) 并指定統(tǒng)一的回調(diào)函數(shù)dispatchEvent(不處理具體的事件,僅對(duì)事件進(jìn)行分發(fā))
存儲(chǔ)事件回調(diào)
React 為了在觸發(fā)事件時(shí)可以查找到對(duì)應(yīng)的回調(diào)去執(zhí)行,會(huì)把組件內(nèi)的所有事件統(tǒng)一地存放到一個(gè)對(duì)象中(listenerBank)。而存儲(chǔ)方式如上圖,首先會(huì)根據(jù)事件類型分類存儲(chǔ),例如 click 事件相關(guān)的統(tǒng)一存儲(chǔ)在一個(gè)對(duì)象中,回調(diào)函數(shù)的存儲(chǔ)采用鍵值對(duì)(key/value)的方式存儲(chǔ)在對(duì)象中,key 是組件的唯一標(biāo)識(shí) (_rootNodeID),value 對(duì)應(yīng)的就是事件的回調(diào)函數(shù)。例如
{
click: {
key1: fn, //key 是組件的唯一標(biāo)識(shí) (_rootNodeID)
key2: fn
},
change: {
key3: fn,
key4: fn
}
}
ReactBrowserEventEmitter作為事件注冊(cè)入口,擔(dān)負(fù)著事件注冊(cè)和事件觸發(fā)。注冊(cè)事件的回調(diào)函數(shù)由EventPluginHub來(lái)統(tǒng)一管理,根據(jù)事件的類型(type)和組件標(biāo)識(shí)(_rootNodeID)為key唯一標(biāo)識(shí)事件并進(jìn)行存儲(chǔ)。
-
事件觸發(fā)
事件觸發(fā)流程.png
其大致流程如下:
- 觸發(fā)事件,開(kāi)始 DOM 事件流,先后經(jīng)過(guò)三個(gè)階段:事件捕獲階段、處于目標(biāo)階段和事件冒泡階段
- 當(dāng)事件冒泡到 document 時(shí),觸發(fā)統(tǒng)一的事件分發(fā)函數(shù) ReactEventListener.dispatchEvent
- dispatchEvent根據(jù)原生事件對(duì)象(nativeEvent)找到當(dāng)前節(jié)點(diǎn)(即事件觸發(fā)節(jié)點(diǎn))對(duì)應(yīng)的 ReactDOMComponent 對(duì)象
- 事件的合成
(1) 根據(jù)當(dāng)前事件類型生成對(duì)應(yīng)的合成對(duì)象
(2) 封裝原生事件對(duì)象和冒泡機(jī)制
(3) 查找當(dāng)前元素以及它所有父級(jí)
(4)在 listenerBank 中查找事件回調(diào)函數(shù)并合成到 events 中 - 批量執(zhí)行合成事件(events)內(nèi)的回調(diào)函數(shù),
- 如果沒(méi)有阻止冒泡,會(huì)將繼續(xù)進(jìn)行 DOM 事件流的冒泡(從 document 到 window),否則結(jié)束事件觸發(fā)
簡(jiǎn)單點(diǎn)的解釋為:
React不會(huì)將事件處理函數(shù)直接綁定到真實(shí)的節(jié)點(diǎn)上,而是把所有的事件綁定到結(jié)構(gòu)的最外層,使用一個(gè)統(tǒng)一的事件監(jiān)聽(tīng)器。這個(gè)監(jiān)聽(tīng)器維持了一個(gè)映射,保存所有組件內(nèi)部的事件監(jiān)聽(tīng)和處理函數(shù)。當(dāng)事件發(fā)生時(shí),首先被這個(gè)統(tǒng)一的事件監(jiān)聽(tīng)器處理,然后在映射里找到真正的事件處理函數(shù)并調(diào)用。
需要注意的是
- React合成事件的冒泡并不是真的冒泡,而是節(jié)點(diǎn)的遍歷。
- 并不是所有事件都會(huì)委托到document上,前面提到幾乎所有的事件代理(delegate)到document,幾乎說(shuō)明存在例外的情況。例如對(duì)于audio、video標(biāo)簽,存在一些媒體事件(例如onplay、onpause),而這些事件是document不具有的,那么只能在這些標(biāo)簽上進(jìn)行事件綁定,綁定一個(gè)入口分發(fā)函數(shù)(dispatchEvent)。
4. React事件和原生事件有什么區(qū)別
關(guān)于合成事件
合成事件官方文檔:https://react.html.cn/docs/events.html
合成事件作用
- 對(duì)原生事件封裝
在事件回調(diào)方法,方法中的參數(shù) e,其實(shí)不是原生事件對(duì)象e而是react包裝過(guò)的對(duì)象,同時(shí)原生事件對(duì)象被放在了屬性 e.nativeEvent內(nèi)。
官網(wǎng)中介紹到了 SyntheticEvent是react合成事件的基類,定義了合成事件的基礎(chǔ)公共屬性和方法。react會(huì)根據(jù)當(dāng)前的事件類型來(lái)使用不同的合成事件對(duì)象,比如鼠標(biāo)單機(jī)事件 - SyntheticMouseEvent,焦點(diǎn)事件-SyntheticFocusEvent等,但是都是繼承自SyntheticEvent。 - 對(duì)原生事件的升級(jí)和改造
對(duì)于有些dom元素事件,我們進(jìn)行事件綁定之后,react并不是只處理你聲明的事件類型,還會(huì)額外的增加一些其他的事件,幫助我們提升交互的體驗(yàn)。
最典型的例子就是input的onChange事件。
image.png -
瀏覽器事件的兼容處理
react在給document注冊(cè)事件的時(shí)候也是對(duì)兼容性做了處理。下面這個(gè)代碼就是給document注冊(cè)事件,內(nèi)部其實(shí)也是做了對(duì) ie瀏覽器的兼容做了處理。
image.png
React事件和原生事件主要區(qū)別有:
- React 組件上聲明的事件沒(méi)有綁定在 React 組件對(duì)應(yīng)的原生 DOM 節(jié)點(diǎn)上。
- React 利用事件委托機(jī)制,將幾乎所有事件的觸發(fā)代理(delegate)在 document 節(jié)點(diǎn)上,事件對(duì)象(event)是合成對(duì)象(SyntheticEvent),不是原生事件對(duì)象,但通過(guò) nativeEvent 屬性訪問(wèn)原生事件對(duì)象。
- 由于 React 的事件委托機(jī)制,React 組件對(duì)應(yīng)的原生 DOM 節(jié)點(diǎn)上的事件觸發(fā)時(shí)機(jī)總是在 React 組件上的事件之前。
- 原生事件阻止冒泡肯定會(huì)阻止合成事件的觸發(fā), 合成事件的阻止冒泡不會(huì)影響原生事件, 兩者最好不要混合使用,避免出現(xiàn)一些奇怪的問(wèn)題。
合成事件優(yōu)點(diǎn):
- 減少內(nèi)存消耗,提升性能,不需要注冊(cè)那么多的事件了,一種事件類型只在 document 上注冊(cè)一次
- 統(tǒng)一規(guī)范,解決兼容問(wèn)題,簡(jiǎn)化事件邏輯,事件處理程序接收到的是SyntheticEvent的實(shí)例。SyntheticEvent完全符合W3C的標(biāo)準(zhǔn),因此在事件層次上具有瀏覽器兼容性。
- React打算干預(yù)事件的分發(fā)。v16引入Fiber架構(gòu),React為了優(yōu)化用戶的交互體驗(yàn),會(huì)干預(yù)事件的分發(fā)。不同類型的事件有不同的優(yōu)先級(jí),比如高優(yōu)先級(jí)的事件可以中斷渲染,讓用戶代碼可以及時(shí)響應(yīng)用戶交互
站在巨人的肩膀上(參考資料)
dom事件
React事件機(jī)制和未來(lái) 這個(gè)文章寫得好!


