前言
在構(gòu)建現(xiàn)代 Web 應(yīng)用時,我們經(jīng)常需要處理大量數(shù)據(jù),并將其渲染為列表或表格,對每個數(shù)據(jù)項綁定一個事件處理程序是常見的做法。但隨著數(shù)據(jù)量的增長,這種做法將會導(dǎo)致嚴重的性能問題;本文旨在介紹事件代理這一解決方案,幫助開發(fā)者優(yōu)化 Web 應(yīng)用性能,提升用戶體驗。
常見場景
在構(gòu)建現(xiàn)代 Web 應(yīng)用時,我們會使用各種現(xiàn)代 Web 框架來簡化我們的工作。以 Vue 為例,渲染一個列表的代碼大致如下:
<template>
<ul>
<li v-for="item of someList" :key="item.id" @click="handleItemClick">{{ item.name }}</li>
</ul>
</template>
<script setup lang="ts">
const someList = [
...
];
const handleItemClick = (e: Event) => {
...
}
</script>
someList 中存在多少條數(shù)據(jù),頁面中便會存在多少個事件監(jiān)聽處理程序。當 someList 數(shù)據(jù)量巨大時,大量的事件監(jiān)聽器會占用大量內(nèi)存,并導(dǎo)致嚴重的性能問題。此時我們可以通過事件代理,將事件處理代理給父元素。事件代理利用了事件冒泡的機制,使得只需在父元素上綁定一個事件監(jiān)聽器,即可處理所有子元素的事件,從而大大減少事件監(jiān)聽器的數(shù)量,代碼如下:
<template>
<ul @click="handleListClick">
<li v-for="item of someList" :key="item.id" :data-id="item.id">{{ item.name }}</li>
</ul>
</template>
<script setup lang="ts">
const someList = [
...
];
const handleListClick= (e: Event) => {
const { id } = e.target.dataset;
if (id !== undefined) {
...
}
}
</script>
通過將子元素需要的數(shù)據(jù)綁定在 dataset 屬性中,我們可以方便的在事件代理中獲取這些數(shù)據(jù),解決了參數(shù)傳遞的問題。然而實際開發(fā)中,列表項遠比示例中要復(fù)雜得多,可能會包含多個圖標、按鈕或其他子元素。
此時我們就需要一種方法來區(qū)分不同的按鈕;同樣我們可以使用 dataset 屬性,為不同的按鈕設(shè)置不同的 dataset 屬性值,然后在事件處理程序中通過 event.target.dataset 來判斷哪個按鈕觸發(fā)了事件,代碼如下;
<template>
<ul @click="handleListClick">
<li v-for="item of someList" :key="item.id">
<p>文案</p>
<button :data-id="item.id" data-event="event1">button1</button>
<button :data-id="item.id" data-event="event2">button2</button>
</li>
</ul>
</template>
<script setup lang="ts">
const someList = [
...
];
const handleListClick= (e: Event) => {
const { id, event } = e.target.dataset;
if (id === undefined || event === undefined) return;
if (event === "event1") {
...
} else if (event === "event2") {
...
}
}
</script>
有時,列表子項內(nèi)包含多個無需任何事件處理程序的子元素,而數(shù)據(jù)綁定在列表子項的 dataset 屬性上。此時,如果點擊子元素, e.target 指向的是子元素,而非列表元素,無法直接獲取 dataset。 e.target.closest(selector) 方法可以向上查找匹配選擇器的最近祖先元素,從而解決這個問題,代碼如下:
<template>
<ul @click="handleListClick">
<li v-for="item of someList" :key="item.id" :data-uid="item.uid">
<p>文案</p>
<img src="../avatar.png" />
</li>
</ul>
</template>
<script setup lang="ts">
const someList = [
...
];
const handleListClick= (e: Event) => {
let { uid } = e.target.dataset;
if (uid === undefined) {
const listItem = e.target.closest("li")
if (!listItem) return;
uid = listItem.dataset.uid;
}
....
}
</script>
結(jié)語
需要注意的是,事件代理并非適用于所有場景,開發(fā)者需要針對具體情況進行實際權(quán)衡。在需要處理大量相似元素的事件時,事件代理無疑是一種優(yōu)秀的解決方案。本文的代碼示例中以 Vue 框架為示例,但核心思想應(yīng)用在 React 或其他框架上也同樣使用,希望本文能夠幫助開發(fā)者更好的使用事件代理,在實際開發(fā)中做出更穩(wěn)妥的選擇。