前端進階之路:點選事件繫結

發表於2015-03-24

引言

前端之所以被稱為前端,是因為它是整個 Web 技術棧中距離使用者最近、直接與使用者進行互動的一環。而網頁介面與使用者的互動通常是通過各種事件來達成的;在各種事件之中,點選事件 往往又是最常見、最通用的一種介面事件。

本文將介紹我在 “點選事件繫結” 這一場景下的進階之路。

背景

我是一個前端小兵,我在一家網際網路公司做做一些簡單的業務開發。

某一天,我接到了一個需求,做一個抽獎功能。公司裡的前輩們已經完成了業務邏輯,而且已經提供了業務功能的介面,只需要我製作頁面並完成事件繫結即可。

實踐

開動

我寫好了頁面,頁面中有一個 ID 為 lucky-draw 的按鈕元素。接下來,我需要為它繫結點選事件。我是這樣寫的:

這其中 BX.luckyDraw() 就是前輩們提供的業務介面,執行它就可以執行後續的抽獎功能。

我測試了一下,程式碼工作正常,於是很開心地準備上線。

第一關

然而前輩們告訴我,這些重要功能的按鈕是需要加統計的。這也難不倒我,因為我很熟悉統計系統的 API。於是我修改了一下事件繫結的程式碼:

這樣做是有效的,但前輩們又告訴我,因為某些原因,統計程式碼和業務程式碼是分佈在不同位置的,以上程式碼需要拆開。於是我嘗試這樣修改:

結果發現點選按鈕時的抽獎功能失效了。原來,使用 .onclick 這樣的事件屬性來繫結事件有一個非常大的缺點,重複賦值會覆蓋舊值。也就是說,這種方式只能繫結最後一次賦值的事件處理函式。

我硬著頭皮去請教前輩,才知道原來這種方式早已經不推薦使用了,應該使用 DOM 標準的事件繫結 API 來處理(在舊版 IE 下有一些相容性問題,這裡不展開)。因此我的程式碼改成了這樣:

所有功能終於又正常了,我很開心地準備上線。

第二關

事實證明我還是太天真了,PM 是不會一次性把所有需求都告訴你的。原來,這個抽獎功能還需要做 A/B 測試,也就是說,只有一半的使用者會看到這個抽獎功能。

這意味著使用者的頁面上可能根本沒有 btn 這個元素,那麼 btn.addEventListener(...) 這一句直接就拋錯了。因此,在為按鈕繫結事件處理函式之前,我不得不先判斷一下:

雖然這樣的程式碼在所有使用者的頁面上都可以正常工作,但這些預先判斷看起來很蛋疼啊。我再次帶著疑惑向前輩請教。前輩慈祥地看著我,說出了一句經典名言:

傻瓜,為什麼不用萬能的 jQuery 呢?

原來,神奇的 jQuery 允許我們忽略很多細節,比如這種沒有取到元素的情況會被它默默地消化掉。而且 jQuery 的事件繫結方法也不存在相容性問題,API 也比較好看。不錯不錯,不管網上的大神們怎麼噴 jQuery,但它簡直是我的救星啊!

於是,我的程式碼變成了以下這樣:

我的程式碼看起來像那麼回事了,我很開心地準備上線。

第三關

當然,我的故事不會這麼快結束。要知道,對一個有追求的前端團隊來說,不斷提升使用者體驗是永恆的目標。比如,我們網站使用了一些方法來提升頁面載入效能,部分頁面內容並不是原本存在於頁面中的,而是在使用者需要時,由 JavaScript 動態生成的。

拿這個抽獎功能來說,抽獎按鈕存在於一個名為 “驚喜” 的 tab 中,而這個 tab 在初始狀態下是沒有內容的,只有當使用者切換到這個 tab 時,才會由 JS 填充其內容。示意程式碼是這樣的:

這意味著,我寫的事件繫結程式碼需要寫在 // BTN READY 處。這種深層的耦合看起來很不理想,我需要想辦法解決它。

我想起來,我在閱讀 jQuery 文件時看到有一種叫作 “事件委託” 的方法,可以在元素還未新增到頁面之前就為它繫結事件。於是,我嘗試這樣來寫:

果然,我成功了!好事多磨啊,這個需求終於開心地上線了。

經過進一步的研究,我瞭解到 “事件委託” 的本質是利用了事件冒泡的特性。把事件處理函式繫結到容器元素上,當容器內的元素觸發事件時,就會冒泡到容器上。此時可以判斷事件的源頭是誰,再執行對應的事件處理函式。由於事件處理函式是繫結在容器元素上的,即使容器為空也沒有關係;只要容器的內容新增進來,整個功能就是準備就緒的。

雖然事件委託的原理聽起來稍有些複雜,但由於 jQuery 對事件委託提供了完善的支援,我的程式碼並沒有因此變得很複雜。

多想一步

經過這一番磨鍊,我收穫了很多經驗值;同時,我也學會了更進一步去發現問題和思考問題。比如,在我們的網頁,通常會有多個按鈕,那為它們繫結事件的指令碼程式碼可能就是這樣的:

我隱隱覺得這樣不對勁啊!雖然這些程式碼可以正常工作,但每多一個按鈕就要為 body 元素多繫結一個事件處理函式;而且根據直覺,這樣一段段長得差不多的程式碼是需要優化的。因此,如果我可以把這些類似的程式碼整合起來,那不論是在資源消耗方面,還是在程式碼組織方面,都是有益的。

於是,我嘗試把所有這些事件委託的程式碼合併為一次繫結。首先,為了實現合併,我需要為這些按鈕找到共同點。很自然地,我讓它們具有相同的 class:

然後,我試圖通過一次事件委託來處理所有這些按鈕:

很顯然,所有具有 action 類名的元素被點選後都會觸發上面這個事件處理函式。那麼,接下來,我們在這裡區分一下事件源頭,並執行對應的任務:

這樣一來,所有分散的事件委託程式碼就被合併為一處了。在這個統一的事件處理函式中,我們使用 ID 來區分各個按鈕。

但 ID 有一些問題,由於同一頁面上不能存在同名的元素,相信前端工程師們都對 ID 比較敏感,在日常開發中都儘量避免濫用。此外,如果多個按鈕需要執行的任務相同,但它的 ID 又必須不同,則這些 ID 和它們對應的任務之間的對應關係就顯得不夠明確了。

於是,我改用 HTML5 的自定義屬性來標記各個按鈕:

我在這裡使用了 data-action 這個屬性來標記各個按鈕元素被點選時所要執行的動作。回過頭看,由於各個按鈕都使用了這個屬性,它們已經具備了新的共同點,而 class 這個共同點就不必要了,於是我們的 HTML 程式碼可以簡化一些:

同時 JS 程式碼也需要做相應調整:

我們的程式碼看起來已經挺不錯了,但我已經停不下來了,還要繼續改進。那個長長的 switch 語句看起來有點臃腫。通常優化 switch 的方法就是使用物件的鍵名和鍵值來組織這種對應關係。於是我繼續改:

經過這樣的調整,我發現程式碼的巢狀變淺了,而且按鈕們的標記和它們要做的事情也被組織成了actionList 這個物件,看起來更清爽了。

在這樣的組織方式下,如果頁面需要新增一個按鈕,也很容易做擴充套件:

到這裡,這一整套實踐終於像那麼回事了!

開源

我自己用這一套方法參與了很多專案的開發,在處理事件繫結時,它節省了我很多的精力。我忽然意識到,它可能還適合更多的人、更多的專案。那不妨把它開源吧!

於是我釋出了 Action 這個專案。這個小巧的類庫幫助開發者輕鬆隨意地繫結點選事件,它使用 “動作” 這個概念來標記按鈕和它被點選後要做的事情;它提供的 API 可以方便地定義一些動作:

也可以手動觸發已經定義的動作:

應用

Action 這個類庫已經被移動 Web UI 框架 CMUI 採用,作為全域性的基礎服務。CMUI 內部的各個 UI 元件都是基於 Action 的事件繫結機制來實現的。我們這裡以對話方塊元件為例,來看看 Action 在 CMUI 中的應用(示意程式碼):

只要當 CMUI.dialog.init() 方法執行後,對話方塊元件就準備就緒了。我們在業務中直接呼叫CMUI.dialog.open() 方法、傳入構造對話方塊所需要的一些配置資訊,這個對話方塊即可建立並開啟。

大家可以發現,在構造對話方塊的過程中,我們沒有做任何事件繫結的工作,對話方塊的關閉按鈕就自然具備了點選關閉功能!原因就在於關閉按鈕(<a href="#" data-action="close-dialog">×</a>)自身已經通過data-action 屬性宣告瞭它被點選時所要執行的動作('close-dialog'),而這個動作早已在元件初始化時(CMUI.dialog.init())定義好了。

結語

希望本文對你有所啟發,也希望 Action 能在實際開發中幫到你。

關於更多細節,歡迎繼續閱讀:

相關文章