1. 程式人生 > 實用技巧 >關於在非同步操作中訪問React事件物件的小問題

關於在非同步操作中訪問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,這樣的遊戲體驗可想而知。