抱歉在掘金頁內連接失效,後續會改正html
查看原文前端
本文經過對React事件系統和源碼進行淺析,回答「爲何React須要本身實現一套事件系統?」和「React的事件系統是怎麼運做起來的?」兩個問題。React爲了性能和複用,採用了事件代理,池,批量更新,跨瀏覽器和跨平臺兼容等思想,將事件監聽掛載在document上,構造合成事件,而且在內部模擬了一套捕獲和冒泡並觸發回調函數的機制,實現了本身的一套事件系統。react
最近在使用React對項目前端進行重構的時候,本身和同事遇到了一些奇怪的問題。因此花了一些時間對React源碼進行了研究,此篇的主題爲React事件系統,儘可能剔除複雜的技術細節,但願能以簡單直觀的方式回答兩個問題,分別是**「爲何React須要本身實現一套事件系統?」和「React的事件系統是怎麼運做起來的?」**。git
Stuff can sometimes get surprisingly messy if you don’t know how it works…github
根據下面代碼,點擊按鈕以後,輸出結果會是什麼?(ABCD排序)web
若是我把innerClick
中的e.stopPropagation()
加上,輸出結果又會是什麼?(ABCD排序)算法
class App extends React.Component {
innerClick = e => {
console.log("A: react inner click.");
// e.stopPropagation();
};
outerClick = () => {
console.log("B: react outer click.");
};
componentDidMount() {
document
.getElementById("outer")
.addEventListener("click", () => console.log("C: native outer click"));
window.addEventListener("click", () =>
console.log("D: native window click")
);
}
render() {
return (
<div id="outer" onClick={this.outerClick}> <button id="inner" onClick={this.innerClick}> BUTTON </button> </div>
);
}
}
複製代碼
正確答案是(防止大家偷看,請向左滑動 <—— ):shell
1.
C: native outer click
A: react inner click.
B: react outer click.
D: native window click
2.
C: native outer click
A: react inner click.
複製代碼
一個表單,預期爲須要點擊按鈕edit以後才能夠進行編輯,而且此時Edit按鈕變爲submit按鈕,點擊submit按鈕提交表單。代碼以下api
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
editable: false
};
}
handleClick = () => {
console.log("edit button click!!");
this.setState({ editable: true });
};
handleSubmit = e => {
console.log("submit event!!");
e.preventDefault(); //避免頁面刷新
};
render() {
return (
<form onSubmit={this.handleSubmit}> {this.state.editable ? ( <button type="submit">submit</button> ) : ( <button type="button" onClick={this.handleClick}>edit</button> )} </form>
);
}
}
複製代碼
但實際上咱們發現,點擊edit按鈕的時候就已經觸發form的submit事件了。爲何咱們點擊了一個type="button"
的按鈕會觸發submit
事件呢?數組
帶着對這兩個例子的思考,咱們進入到本文的主題。我只想直接看答案?
我認爲這個問題主要是爲了性能和複用兩個方面來考慮。
首先對於性能來講,React做爲一套View層面的框架,經過渲染獲得vDOM,再由diff算法決定DOM樹那些結點須要新增、替換或修改,假如直接在DOM結點插入原生事件監聽,則會致使頻繁的調用addEventListener
和removeEventListener
,形成性能的浪費。因此React採用了事件代理的方法,對於大部分事件而言都在document上作監聽,而後根據Event中的target來判斷事件觸發的結點。(除了少數不會冒泡到document的事件,例如video等。)
其次React合成的SyntheticEvent
採用了池的思想,從而達到節約內存,避免頻繁的建立和銷燬事件對象的目的。這也是若是咱們須要異步使用一個syntheticEvent
,須要執行event.persist()
才能防止事件對象被釋放的緣由。
最後在React源碼中隨處可見batch作批量更新,基本上凡是能夠批量處理的事情(最廣泛的setState
)React都會將中間過程保存起來,留到最後面才flush掉。就如瀏覽器對DOM樹進行Style,Layout,Paint同樣,都不會在操做ele.style.color='red';
以後立刻執行,只會將這些操做打包起來並最終在須要渲染的時候再作渲染。
ele.style.color='red';
ele.style.color='blue';
ele.style.color='red';
瀏覽器只會渲染一次
複製代碼
而對於複用來講,React看到在不一樣的瀏覽器和平臺上,用戶界面上的事件其實很是類似,例如普通的click
,change
等等。React但願經過封裝一層事件系統,將不一樣平臺的原生事件都封裝成SyntheticEvent
。
ReactBrowserEventEmitter
,Native上加入ReactNativeEventEmitter
。以下圖,對於不一樣平臺,React只須要替換掉左邊部分,而右邊EventPluginHub
部分能夠保持複用。transitionEnd
,webkitTransitionEnd
,MozTransitionEnd
和oTransitionEnd
, React都會集合成topAnimationEnd
,因此咱們只用處理這一個標準的事件便可。簡單而言,就與jQuery幫助咱們解決了不一樣瀏覽器之間的兼容問同樣,React更進一步,還幫咱們統一了不一樣平臺的兼容,使咱們在開發的時候只須要考慮標準化的事件便可。
咱們來看一下咱們在JSX中寫的onClick
handler是怎麼被記錄到DOM結點上,而且在document
上作監聽的。
React對於大部分事件的綁定都是使用trapBubbledEvent
和trapCapturedEvent
這兩個函數來註冊的。如上圖所示,當咱們執行了render
或者setState
以後,React的Fiber調度系統會在最後commit到DOM樹以前執行trapBubbledEven
或trapCapturedEvent
, 經過執行addEventListener
在document結點上綁定對應的dispatch
做爲handler負責監聽類型爲topLevelType
的事件。
這裏面的dispatchInteractiveEvent
和dispatchEvent
兩個回調函數的區別爲,React16開始換掉了本來Stack Reconciliation成Fiber但願實現異步渲染(目前仍未默認打開,仍需使用unstable_
開頭的api,此特性與例子2有關,將在文章最後配圖解釋),因此異步渲染的狀況下加入我點了兩次按鈕,那麼第二次按鈕響應的時候,可能第一次按鈕的handlerA中調用的setState
還未最終被commit到DOM樹上,這時須要把第一次按鈕的結果先給flush掉並commit到DOM樹,纔可以保持一致性。這個時候就會用到dispatchInteractiveEvent
。能夠理解成dispatchInteractiveEvent
在執行前都會確保以前全部操做都已最總commit到DOM樹,再開始本身的流程,並最終觸發dispatchEvent
。但因爲目前React還是同步渲染的,因此這兩個函數在目前的表現是一致的,但願React17會帶給咱們默認打開的異步渲染功能。
到如今咱們已經在document結點上監聽了事件了,如今須要來看如何將咱們在jsx中寫的handler存起來對應到相應的結點上。
在咱們每次新建或者更新結點時,React最終會調用createInstance
或者commitUpdate
這兩個函數,而這兩個函數都會最終調用updateFiberProps
這個函數,將props
也就是咱們的onClick
,onChange
等handler給存到DOM結點上。
至此,咱們咱們已經在document上監聽了事件,而且將handler存在對應DOM結點。接下來須要看React怎麼監聽並處理瀏覽器的原生事件,最終觸發對應的handler了。
這裏我作了個動畫,但願可以對大家理解有幫助。點擊綠色的按鈕>播放下一步。
抱歉須要插入連接,掘金不容許插入iframe。
以簡單的click
事件爲例,經過事件綁定咱們已經在document
上監聽了click
事件,當咱們真正點擊了這個按鈕的時候,原生的事件是如何進入React的管轄範圍的?如何合成SyntheticEvent
以及如何模擬捕獲和冒泡的?以及最後咱們在jsx中寫的onClick
handler是如何被最終觸發的?帶着這些問題,咱們一塊兒來看一下事件觸發階段。
我會大概用下圖這種方式來解析代碼,左邊是我點擊一個綁定了handleClick
的按鈕後的js調用棧,右邊是每一步的代碼,均已刪除部分不影響理解的代碼。但願經過這種方式能使你們更易理解React的事件觸發機制。
當咱們點擊一個按鈕是,click
事件將會最終冒泡至document,並觸發咱們監聽在document上的handler dispatchEvent
,接着觸發batchedUpdates
。batchedUpdates
這個格式的代碼在React的源碼裏面會頻繁的出現,基本上React將全部可以批量處理的事情都會先收集起來,再一次性處理。
能夠看到默認的isBatching
是false的,當調用了一次batchedUpdates
,isBatching
的值將會變成true,此時若是在接下來的調用中有繼續調用batchedUpdates
的話,就會直接執行handleTopLevel
,此時的setState
等不會被更新到DOM上。直到調用棧從新回到第一次調用batchedUpdates
的時候,纔會將全部結果一塊兒flush掉(更新到DOM上)。
有的同窗可能問調用棧中的BatchedUpdates$1
是什麼?或者瀏覽器的renderer和Native的renderer是若是掛在到React的事件系統上的?
其實React事件系統裏面提供了一個函數setBatchingImplementation
,用來動態掛載不一樣平臺的renderer,這個也體現了React事件系統的複用
。
這裏的interactiveUpdates
和batchedUpdates
的區別在上文已經解釋過,這裏就再也不贅述。
handleTopLevel
會調用runExtractedEventsInBatch()
,這是React事件處理最重要的函數。如上面動畫咱們看到的,在EventEmitter
裏面作的事,其實主要就是這個函數的兩步。
首先調用extractEvents
,傳入原生事件e
,React事件系統根據可能的事件插件合成合成事件Synthetic e
。 這裏咱們能夠看到調用了EventConstructor.getPooled()
,從事件池中去取一個合成事件對象,若是事件池爲空,則新建立一個合成事件對象,這體現了React爲了性能實現了池的思想。
而後傳入Propagator,在vDOM上模擬捕獲和冒泡,並收集全部須要執行的事件回調和對應的結點。traverseTwoPhase
模擬了捕獲和冒泡的兩個階段,這裏實現很巧妙,簡單而言就是正向和反向遍歷了一下數組。接着對每個結點,調用listenerAtPhase
取出事件綁定時掛載在結點上的回調函數,把它加入回調數組中。
接着遍歷全部合成事件。這裏能夠看到當一個事件處理完的時候,React會調用event.isPersistent()
來查看這個合成事件是否須要被持久化,若是不須要就會釋放這個合成事件,這也就是爲何當咱們須要異步讀取操做一個合成事件的時候,須要執行event.persist()
,否則React就是在這裏釋放掉這個事件。
最後這裏就是回調函數被真正觸發的時候了,取出回調數組event._dispatchListeners
,遍歷觸發回調函數。並經過event.isPropagationStopped()
這一步來模擬中止冒泡。這裏咱們能夠看到,React在收集回調數組的時候並不會去管咱們是否調用了stopPropagation
,而是會在觸發的階段纔會去檢查是否須要中止冒泡。
至此,一個事件回調函數就被觸發了,裏面若是執行了setState
等就會等到調用棧彈回到最低部的interactiveUpdate
中的被最終flush掉,構造vDOM,和好,並最終被commit到DOM上。
這就是事件觸發的整個過程了,能夠回去再看一下動畫,相信你會更加理解這個過程的。
如今咱們對React事件系統已經比較熟悉了,回到文章開頭的那兩個玄學問題,咱們來看一下到底爲何?
若是想看題目內容或者忘記題目了,能夠點擊這裏查看。
相信看完這篇文章,若是你已經對React事件系統有所理解,這道題應該是不難了。
#outer
上監聽的回調C
會最早被輸出;接着原生事件冒泡至document進入React事件系統,React事件系統模擬捕獲冒泡輸出A
和B
;最後React事件系統執行完畢回到瀏覽器繼續冒泡到window,輸出D
。#outer
上監聽的回調C
會最早被執行;接着原生事件冒泡至document進入React事件系統,輸出A
,在React事件處理中#inner
調用了stopPropagation
,事件被中止冒泡。因此,最好不要混用React事件系統和原生事件系統,若是混用了,請保證你清楚知道會發生什麼。
若是想看題目內容或者忘記題目了,能夠點擊這裏查看。
這個問題就稍微複雜一點。首先咱們點擊edit
按鈕瀏覽器觸發一個click
事件,冒泡至document進入React事件系統,React執行回調調用setState
,此時React事件系統對事件的處理執行完畢。因爲目前React是同步渲染的,因此接着React執行performSyncWork
將該button改爲type="submit"
,因爲同個位置的結點而且tag都爲button,因此React複用了這個button結點(具體緣由能夠參考)並更新到DOM上。此時瀏覽器對click
事件處理執行繼續,發現該結點的type="submit"
,又在form下面,則對應觸發submit
事件。
解決的辦法就有不少種了,給button加上key
;兩個按鈕分開寫,不要用三元等均可以解決問題。
具體能夠看一下下面的這個調用圖,應該也很好理解,若是有不能理解的地方,請在下面留言,我會盡我所能解釋清楚。
相信對於不少React開發者來講,「setState是異步的」這句話應該常常聽到,我記得我一開始學習React的時候常常就會看到這句話,而後說若是須要用到以前的state須要在setState中採用setState((preState)=>{})
這樣的方式。
但其實這句話並非徹底準確的。準確的說法應該是setState有時候是異步的,setState相對於瀏覽器而言是同步的
目前而言setState
在生命週期以及事件回調中是異步的,也就是會收集起來批量處理。在其它狀況下如promise,setTimeout中都是同步執行的,也就是調用一次setState就會render一次並更新到DOM上面,不信的話能夠點擊這裏嘗試。
且在JS調用棧被彈空時候,一定是已經將結果更新到DOM上面了(同步渲染)。這也就是setState相對於瀏覽器是同步的含義。以下圖所示
異步渲染的流程圖大概以下圖所示,最近一次思考這個問題的時候,發現若是如今是異步渲染的話,那咱們的例子二將變成偶現的坑😂,由於若是setState
的結果還沒被更新到DOM上,瀏覽器就不會觸發submit事件。
不過React團隊已經爲異步渲染的願景開發了兩年,且React16中已經採用了Fiber reconciliation和提供了異步渲染的api unstable_xxx
,相信在React17中咱們能夠享受到異步渲染帶來的性能提高,感謝React團隊。
但願讀完此文,能讓你React事件系統有個簡單的認識。知道「爲何React須要本身實現一套事件系統?」和「React的事件系統是怎麼運做起來的?」。React爲了性能和複用,採用了事件代理,池,批量更新,跨瀏覽器和跨平臺兼容等思想,將事件監聽掛載在document上,而且構造合成事件,而且在內部模擬了一套捕獲和冒泡並觸發回調函數的機制,實現了本身一套事件系統。
若是你還有哪裏不清楚,發現文章有錯漏,或是單純的交流相關問題,請在下面留言,我會盡我所能回覆和解答你的疑問的。 若是你喜歡個人文章,請關注我和個人博客,謝謝。