升級 React 17 要注意的事件影響

Jason Tseng
Oct 29, 2020

--

雖然 React 官方說 17 是一版過渡的版本,沒有新功能,沒有改變任何現況,然而可能會影響事件的捕獲冒泡:

onClick = () => {
document.addEventListener('click', console.log);
};
<button onClick={this.onClick}>
onClick
</button>

在 16 版以前, event 會在 this.onClick() 後才建立,然而在 17 版,會立即建立並觸發 click 導致印出 console.log(event);

onClick = (event) => {
event.stopPropagation(); // 本來不用加,現在要了
document.addEventListener('click', console.log);
};
<button onClick={this.onClick}>
onClick
</button>

這不屬於 16 或 17 的 bug。這牽扯到 React 實作內部事件是使用 Synthetic event 的技術,為了優化效能,目的是讓所有事件集中到 document 去觸發。然而 16 升級至 17 時,React 移動的所有位在 document 集合的事件(實作 Event delegation)移動到 div#root 的位置,如下圖:

https://reactjs.org/static/bb4b10114882a50090b8ff61b3c4d0fd/31868/react_17_delegation.png

於這張圖說明一切:程式碼部分原本位在 document 的 click 觸發後並且馬上在 document 增加另一個 onClick listener,沒有被 bubble 給捕捉到;但是在 17 版以後,是在 div#root 這個層級加入 element onClick 的,在這層 code 添加新的 listener 到 document onClick,這時會因為 JavaScript 本身是單線程的程式語言給觸發( JavaScript 允許程式跑一半時改寫 DOM 結構,導致他必須是單線程語言),當 div#root 的 onClick 裡的程式跑完後,接下來會因為 Event bubble 的關係往外找有沒有其他的 onClick 存在,這時添加的 listener 已經準備完畢,所以就會馬上接著觸發。

即使使用 onClickCapture 也會有一樣的行為,因為事件傳遞是先捕獲、後冒泡,後面還是會觸發到下一個 onClick。

大家要留意可能潛在的 bug,可能會觸發非預期行為:這種寫法不常見,但是仍會寫在套件 Dropdown 的關閉、modal 的關閉等需要大範圍 click 的功能。

Keywords:
React、Synthetic event、Event bubbling、Event capturing、Event delegation

Reference:
React v17.0
DOM 的事件傳遞機制:捕獲與冒泡

--

--