[譯] 模組化系統中的 event.stopPropagation()

掘金翻譯計劃發表於2019-03-03
[譯] 模組化系統中的 event.stopPropagation()

在 Moxio,我們通過叫 widgets 的模組來構建網路應用。一個 widget 裡面包含一些邏輯,它將控制一小部分 HTML。就像是 checkbox 元素或者一組其它的 widgets。一個 widget 可以申明它需要的資料和依賴關係,並且可以選擇傳遞資源去它的子元件。模組化可以很好的來管理複雜度,因為所有的資源傳輸的渠道都被很明確的定義了。模組化也可以允許你通過不同的組合方式來複用 widgets。JavaScript 想要真正的確保模組化約定是有點小困難的,因為你總是可以訪問全域性作用域,當然,我們也有辦法來解決這個問題。

JavaScript 中的模組化設計

原生 JavaScript 的 API 在設計中並沒有考慮到模組化;預設情況下,你可以訪問到全域性作用域(global_ scope)。我們通過將全域性資源封裝在根目錄並向下層傳遞的方式,來讓 wigets 獲得到這些資源。我們對一些資源進行了封裝,比如 LocalStorage,頁面的 URL 以及 viewport(為了觀察在頁面內的座標)。我們還封裝 DOMElements 和事件。通過這些封裝器,我們可以限制和調整功能,進而保證模組化約定的完整。例如:一個 click 事件 可能知道 shift 鍵是否被按,但是你沒法知道 click 事件的目標是什麼,因為該點選事件的目標可能是在另一個 widget 內。這個看起來可能有非常大的限制性,但是直到目前,我們還沒有發現需要直接暴露目標的需求。

對於每一個特徵,我們都找到了一種不破化模組化約定的方法來表達它們。這也引出了我對於 event.stopPropagation() 的分析。我們是否需要它?我們如何能夠提供它的功能?

stopPropagation 的栗子?

思考一下這個 HTML 的例子:

<div class="table">
    <div class="body">
        <div class="row open">
            <div class="columns">
                <div class="cell">
                    <span class="bullet"></span>
                    <input type="checkbox" />
                    Lorem ipsum dolor sit amet
                </div>
                <div class="cell"><a href="/lorem-ipsum">Lorem ipsum</a></div>
            </div>
            <div class="contents">
                <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
            </div>
        </div>
        <!-- more rows -->
    </div>
</div>
複製程式碼

加了一點 CSS 後它變成了這樣:

[譯] 模組化系統中的 event.stopPropagation()

我們有如下一些互動:

  • 點選 checkbox 將選中和取消它,並且使得所在行被“選擇”
  • 點選第二格里的連結將會開啟對應地址
  • 點選任何一行將會開啟或者關閉該行下顯示“內容”

JavaScript 的事件模型

讓我們一起快速的過一遍事件在 JavaScript 中是怎麼運作的。當你點選一個元素節點(例如一個 checkbox),一個事件誕生,首先它沿著節點樹向下傳遞:table > body > row > columns > cell > input。這是捕捉(capturing)階段。然後,這個事件按照相反的順序向上傳遞,這個冒泡(bubble)階段:input > cell > columns > row > body > table.

這意味著,對於 checkbox 的點選會造成一個在 checkbox 和 row 上的 click 事件。我們不希望點選 checkbox 會開啟/關閉 row,所以我們需要查明這一點。這裡我們也就引入了 stopPropagation。

function on_checkbox_click(event) {
    toggle_checkbox_state();
    event.stopPropagation(); // prevent this event from bubbling up
}
複製程式碼

如果處於冒泡(bubble)階段時,你在 checkbox 中的 click 事件的監聽器中加入了 event.stopPropagation(),那麼這個事件將不會繼續向上冒泡傳遞,也就永遠不會到達 row 節點。也就簡單明瞭實現了我們所期待的互動。

預期之外的互動

然而,使用 stopPropagation 有一個副作用。點選 checkbox 的事件將 完全 不再向上傳遞。我們的初衷是遮蔽在 row 節點上的點選事件,但我們也遮蔽了所有的父節點。例如說我們有一個如果點選在其他地方,就會被關閉的開啟著的選單。那麼那個簡單明瞭的 click 監聽器就不再適用,因為我們的 click 事件可能會“消失”。我們依舊能夠使用捕捉(capturing)階段,但是又有什麼能夠阻止位於父節點中的一個 widget 來遮蔽掉那個事件呢?stopPropagation 給我們的模組化帶來了矛盾。似乎,在捕捉(capturing)和 冒泡(bubble)階段中,禁止在 widgets 中加入 event propagation 的才是眾望所歸的選擇。

如果我們從封裝器中移除對於 stopPropagation 的支援,我們還能夠實現我們的上述的互動麼?可以的,但是將會很混亂。我們可以做一些簿記,通過記錄的方式知曉什麼時候我們應該忽略 row 節點上的 click 事件,或者我們可以新建一個事件的目標,又或者我們讓你知道事件在哪發生。我們實驗了一些解決方法,但是我們並不太喜歡它們。

通過簿記來解決的例子:

var checkbox_was_clicked = false;

function on_checkbox_click() {
    checkbox_was_clicked = true;
    handle_checkbox_click();
}

function on_row_click() {
    if (checkbox_was_clicked === false) {
        handle_row_click();
    }
    checkbox_was_clicked = false;
}
複製程式碼

你可以看出,當我們希望遮蔽更多的元素節點(例如第二行的連結),或者希望遮蔽的元素在次級的 widget 時,這個解決方法將會變得多麼的笨重。

一個概念上的解決方式

我們可以做的更好。這裡有這樣一個概念。我們還沒有給它想好一個名字,但我們考慮叫它 significant action(重大的動作) 類似的名字。當你 click 時,你總是有一個最主要的動作:不管是開啟/關閉 row 節點 還是 checkbox,但從來不會是二者同時發生。從 UX 設計的角度來說這很道理的。我的第一個想法是 stopPropagation 不應該停止冒泡(bubble),而應該在事件中設定一個標誌來表明,一個重要的動作已經被執行了。這個方法的缺點是對於每一個可互動的元素節點來說(checkbox,link,button 等等),你都需要為它們新增一個事件觸發(handler)來設定這個標誌。那看起來會很是很大的工作量。我們可以稍微改進一點:對於互動元素節點,我們已經知道它們有significant action,所以如果目標是互動元素節點,那麼就自動設定 significant 標誌。當我們把這樣的邏輯實現在我們的事件封裝器時,row 節點現在只需要去檢查 significant 標誌,那麼我們就可以忽略來自第一列的 checkbox 和 第二列的連結的點選事件了。

我們可以這樣實現我們 row 的 click 事件觸發:

function on_row_click(event) {
    if (event.is_handled() === false) { // this event had no significant action
        toggle_row_open_state();
    }
}
複製程式碼

總結

我經常被 JavaScript 和它的原生庫的設計中的前瞻性所驚豔。總體來說,它工作的很好。它那一種選擇你自己的冒險式的 API 支援很多的工作流程,也包括我們的。我們的模組化設計和封裝讓我們可以在原生庫上增加我們的概念。我們可以填海移山。

我們依舊允許 stopPropagation 的使用,但是我們不鼓勵。significant - 標誌已經在很多的 checkbox-table 中實現了,歡樂多多喲。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章