關於在非同步操作中訪問React事件對象的小問題
- 2020 年 9 月 12 日
- 筆記
- javascript, React
最近擼React的程式碼時踩了個關於事件處理的坑,場景如下:在監聽某個元素上會頻繁觸發的事件時,我們往往會對該事件的回調函數進行防抖的處理;防抖的包裝函數大致長這樣:
debounce = (fn, delay) => {
let timer: any = null;
return function(...args) {
if(timer) {
clearTimeout(timer);
}
timer = setTimeout(fn, delay, ...args);
}
}
核心部分就是用setTimeout()
做延時執行,而問題就是出在這裡。先說下結論,在React中如果要在非同步操作中訪問事件對象,則需要先在該事件對象上執行event.persist()。否則的話,在非同步操作中訪問事件對象時你會發現這個對象上大部分屬性都是無效的了。
之前在項目中其他地方也見過這個方法也查了下知道這個東西,不過當時也只是知道有這麼個方法並不太理解這麼個方法存在的意義,現在好了,踩坑了吧,只好專門去了解下其中的緣由(=_=) 。 非同步訪問事件對象時其屬性失效的原因在於事件派發並處理完後 這個對象不會馬上被釋放,而是將這個事件對象上的一些屬性釋放再回收放進被稱為「事件池」的這麼個地方。 看下react-dom中的這段源碼:
在上面步驟中,派發完事件後,會判斷事件對象event.isPersistent()
即是否有被持久化;而如果我沒有在處理函數中執行過event.persist()
,所以就進入了分支執行release操作;執行完release後,這個event上的大部分屬性就都被清空了然後被放進事件池裡。而非同步操作是發生在這個過程之後的,這時候如果要訪問該event的話 例如我們獲取event.target
這時event上的target屬性是不存在的了,程式碼就出錯了。
然後再說下事件池;官方文檔在說明上述問題時提到了下事件池 :
SyntheticEvent 是合併而來。這意味著 SyntheticEvent 對象可能會被重用,而且在事件回調函數被調用後,所有的屬性都會無效。出於性能考慮,你不能通過非同步訪問事件。
說的比較籠統,解釋一下:所有產生的事件都會生成一個事件對象,按正常邏輯 在我們的事件處理函數執行完後,這個事件對象就應該被釋放了,等待著被記憶體回收;但如果在短時間內觸發了許多次事件,就要頻繁的生成和銷毀事件對象;那麼 為了提高性能,React就用了一個「事件池」這麼一個池子,被使用完後的事件,並不直接銷毀,而是將其身上的屬性清空掉了後放進事件池中, 等到了下一次有同類型事件發生時,就不用再new一個新的事件對象了,直接從事件池取出一個現成的就可以用了, 從而實現事件對象的重用。
使用這麼一套機制最根本的動機在於:在很多業務系統中創建和銷毀對象的代價是非常昂貴的。只接觸過前端領域的同學可能沒怎麼聽說過XXX對象池這種概念,不過在其他工種的圈子中這個模式被運用在很多地方, 例如後端中經常提及的執行緒池、資料庫連接池,在遊戲引擎Unity中也有對象池的概念。 這個模式對於一些場景的性能提升是非常大的,我們想像一下這些場景:Web伺服器遇到高並發時,會在瞬時創建和銷毀大量的執行緒、 又或者當我們在愉快地玩耍諸如FPS類型的遊戲時,每個彈藥都是一個遊戲中的對象,那麼就會經常會產生大量的對象,並且在短時間內這些對象又會在使用完後等效被銷毀,勢必就會給遊戲的運行帶來很大負擔;而且很可能還會伴隨著長時間的GC,這樣的遊戲體驗可想而知。