React16中打印事件對象取不到值的現象及其原因分析
一、背景
在最近的開發過程中,遇到了一個看起來匪夷所思的問題?:
<Inputplaceholder="請輸入"onChange={(e) => {console.log('e:', e)}}onKeyDown={handleKeyDown} />
此時按理來說我們只要拿到e.target.value就可以拿到輸入框中的值,我們在輸入框中輸入1
進行測試,然而打印出來卻是這樣的😥:
我們拿到的target是個null,而且事件對象e的各個屬性都是以(...)
的形式出現的,點開以后可以發現居然全是空的😨:
這便引出了第一個問題,但是還沒完,更邪門的問題還在后面🤔:
<Inputplaceholder="請輸入"onChange={(e) => {console.log('e.target.value:', e.target.value)console.log('e:', e) }}onKeyDown={handleKeyDown} />
這段代碼相比較于上一段代碼,只是多打印了一個e.target.value
,從上面的經驗來看,既然e.target
是空的,那么繼續訪問它的value應該會報錯才對,我們再次在輸入框中輸入1
進行測試,然而結果再次出乎意料🤯:
直接打印e.target.value
居然能拿到輸入的1
🤔,這便是我們的第二個問題
二、問題1:事件對象e的屬性為何為空?
要回答這個問題,需要引出我們的標題——‘事件對象池’
1.合成事件 – onXxx={函數}
在介紹事件對象池之前我們需要先簡單介紹下合成事件(SyntheticEvent)的概念,簡單來說就是在 React 中基于onXxx={函數}
的事件處理函數時,React 會將原生 DOM 事件封裝成一個合成事件對象,然后傳遞給事件處理函數。這個合成事件對象包含了與原生 DOM 事件相同的信息,以及一些額外的屬性和方法,用于處理事件。
至于合成事件更具體細致的內容,不是本文的重點,在這里只需要先簡單的認識到這兩點:
- react中的合成事件和我們平時寫的原生事件并不是同一個東西
- 合成事件基于事件委托實現,在react16中委托給document
即可。
如果想要更加細致地學習合成事件相關的知識,這里筆者推薦觀看b站上珠峰React視頻中(https://www.bilibili.com/video/BV1sx4y1L7Rg?p=21&vd_source=9e266526041bdf6f2a69b06653a7eb54)P21-P25。 這個視頻雖然時間較長,但講解較為細致。
或者可以自行尋找一些博客或者文檔學習這一部分的知識。
2.事件對象池
為了避免每次事件觸發都需要重新創建新的合成事件對象,React在16版本中引入了“事件對象池”緩存機制。具體來說,當每次事件觸發傳播到委托的元素上時,React會統一處理內置事件對象生成合成事件對象。然后,React從事件對象池中獲取存儲的合成事件對象,并把信息賦值給相關的成員。等待本次操作結束后,React會把合成事件對象中的成員信息清空,并放回事件對象池中等待下一次使用。這種機制避免了重復創建和銷毀大量的合成事件對象,從而提高了React的性能和效率。
這里的關鍵點就是在于——React會把合成事件對象中的成員信息清空
同時值得注意的是,在 React 17 及其后續版本中,React 已經移除了事件池機制。
3.對問題1的初步解釋
從上面的分析中我們可以得知,在react16版本中存在事件對象池的機制,在操作結束以后,react會把合成事件對象中的成員信息都清空掉,再放入到事件對象池
當中。
這和我們之前 遇到的事件對象e的各個屬性都是以(...)
的形式出現的,點開以后可以發現居然全是空的 這一現象吻合😤。
4.e.persist()
React 提供了 e.persist()
方法。e.persist()
方法的作用就是從事件對象池中移除合成事件對象的引用,使得事件對象在事件處理函數執行結束后仍然可用。它告訴 React 不要在事件處理函數執行完畢后重置合成事件對象。
這里給出react舊版本的官方文檔中有關e.persist()的傳送門:https://legacy.reactjs.org/docs/legacy-event-pooling.html#gatsby-focus-wrapper
由于在2023年的2月份,react的官方文檔已經進行了一次大更新,整個文檔都重寫了一遍,目前的新文檔全面擁抱hooks,同時點擊老文檔的鏈接會直接重定向到最新的文檔,直接從搜索引擎搜索也很難找到老文檔,這里建議有需要的朋友們可以收藏一下。
<Inputplaceholder="請輸入"onChange={(e) => {e.persist()console.log('e:', e)}}/>
此時我們再次輸入1
,可以觀察到此時的事件對象并沒有消失,各個成員信息都回來了😋!
到這里,問題1看似已經得到了解決,但又怎么解釋問題2的現象呢🤔?
三、問題2:直接打印e.target.value為什么能拿到輸入的1
1.問題分析
<Inputplaceholder="請輸入"onChange={(e) => {console.log('e.target.value:', e.target.value)console.log('e:', e) }}/>
這是我們之前的代碼,我們發現e.target.value
居然有值,而e中展開后e.target
卻為null,按理說事件對象e被回收后應該是拿不到值的啊,這就很奇怪了。
難道是代碼執行順序的問題,因為是先打印的e.target.value
后打印的 e
,但是這兩行代碼緊緊挨在一起,執行的時間很短啊,事件對象池不會這么湊巧在這兩行代碼執行的間隙中生效回收事件對象吧。
讓我們將它們調換一下順序試試:
<Inputplaceholder="請輸入"onChange={(e) => {console.log('e:', e)console.log('e.target.value:', e.target.value)}}/>
結果先執行的e中target依然為null
,而后執行的e.target.value
中居然能拿到值!
這個問題筆者也困擾了好久,哪怕是去上網搜索或者借助ai工具也很難得到一個合理的解釋😭。
2.console.log() 的機制
就在筆者幾乎是要放棄的時候,突然想到,這兩個都是通過console.log()
來打印的,那會不會是因為console.log()
存在某種機制,比如說輸出時機上的一些講究,才導致了這一現象呢,只不過是console.log()
實在是太常用了,才導致我們疏忽了。
進入mdn文檔查找后,果然是console.log()
的原因!🤯
mdn文檔中有關console.log() 的部分 : https://developer.mozilla.org/zh-CN/docs/Web/API/console/log_static
也就是說當 console.log()
打印對象時,存在著一套特殊的機制,它將得到該對象的引用,而且輸出的并不是在執行console.log()
時的值,而是打開控制臺時的值。
我們用一段簡單的代碼來驗證一下這個情況:
<Inputplaceholder="請輸入"onChange={(e) => {const obj = { a: '1' }console.log('obj1:', obj)obj.a = '2'}}
/>
盡管看起來obj.a還是1,但是展開以后已經變成了2
甚至我們可以更大膽的嘗試:
<Inputplaceholder="請輸入"onChange={(e) => {const obj = { a: '1' }console.log('obj1:', obj)obj.a = '2'setTimeout(() => {obj.a = '2'}, 500)}}
/>
得到的依然是同一個結果😎
四、總結
1.結論
根據上述討論,我們的邏輯可以得到合理的閉環:
<Inputplaceholder="請輸入"onChange={(e) => {console.log('e:', e)console.log('e.target.value:', e.target.value)}}/>
以這段代碼為例,當我們在輸入框中輸入值的時候,先后觸發了
console.log('e:', e)
console.log('e.target.value:', e.target.value)
這兩句代碼的執行。
在執行第一個 console.log('e:', e)
時,由于e是個對象,根據console.log()
的機制,console.log
此時將得到該對象的引用;
在執行第二個 console.log('e.target.value:', e.target.value)
時,由于e.target.value
并不是對象, console.log()
此時直接得到這個值。
接著等到我們打開控制臺時,由于此時的事件對象e已經被事件對象池
回收了,因此打印出的第一項e中的各個成員信息都是空的,而第二項中的 e.target.value
并不是對象,因此能直接打印出它的值。
2.利用debugger工具來驗證
我們再利用debugger工具來驗證我們的想法:
<Inputplaceholder="請輸入"onChange={(e) => {console.log('e:', e)console.log('e.target.value:', e.target.value)debugger}}/>
通過debugger可以看到,代碼在執行到 console.log()
的時候,此時是有值的:
這也就再一次印證了我們的結論😁。