[译] 模块化系统中的 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 后它变成了这样:

我们有如下一些交互:

  • 点击 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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值