为什么 `.add()` API 没有采用队列机制?
为什么 .add() API 没有采用队列机制?
本问题摘自 C4 前端交流会的现场观众提问。
背景
大家最熟悉的 DOM 事件 API .addEventListener() 是队列机制——可以为元素的同一事件多次添加事件处理函数,当事件触发时,这些事件处理函数会被依次调用。
因此,当大家看到 action.add() 这个 API 时,很自然就会发出疑问:它的工作方式是队列式吗?如果不是,为什么?
文档
实际上 .add() API 并没有设计队列机制。对于重复添加同名动作的情况,文档是这样描述的:
如果在定义动作时使用了已经存在的动作名,则相当于用新的动作函数替换原有的动作函数。原因在于
action.add()方法添加的是 “动作”,不是事件监听器;而每个动作名只能对应一个动作函数。如果你觉得
.add()这个接口名容易误解,可以自行创建并使用.define()或.register()这样的别名。
设计
从上述文档可以看出,Action 的设计力求 “简单易用”:
- 用 “动作” 的概念屏蔽事件、冒泡、监听器等底层实现。
- 动作采用基本的 “名值对” 模型,“一个萝卜一个坑”。
引入队列机制,会使整概念模型复杂化。作为系统中的一项基础设施,Action 必须做到简单、易理解、易排错,因为当大量上层代码依赖这个基础设施时,稳定性必定是它的首要目标。
同时,对一个通用型类库来说,不加节制地增强功能,并不会令它更加通用;相反,膨胀的体积和大多数人用不到的功能反而会限制它的适用面。一个单纯的机制,恰恰更容易被按需扩展。
扩展
每个类库都有适用场景,Action 的原生功能适用于中小型网站项目;而对于更复杂的场景,Action 也可以通过各种方式在上层被扩展。
在 C4 前端交流会上,有同学认为,由于 .add() 没有队列机制(不可以对同一动作追加多个动作函数),无法满足以下需求:
如果我有一组类似的按钮需要执行一些共同的动作、同时也有自己特有的动作,如何实现?
实际上,我们完全可以在动作函数这个层面来完成业务逻辑的抽象。为便于讨论,我们先约定场景和术语:“Btn A” 和 “Btn B” 都需要执行自己的特有行为 fnA 和 fnB,同时它们也需要执行共同行为 fnC。那么我们可以采用以下一些的扩展方案:
-
使用 AOP 等方式来给
fnA和fnB分别追加fnC,得到fnAC和fnBC,再把它们作为真正的动作函数传给.add()方法。 -
.add()只管接收动作列表(用对象组织的名值对),至于这个动作列表是怎么来的,它是不关心的。我们完全可以用一套更高级的机制来构造出自己需要的动作列表。假设我们根据自己的业务需要实现了一套ActionComposer,可以按需构造动作函数:var myActions = { 'action-a': ActionComposer.combine('fnA', 'fnC'), 'action-b': ActionComposer.combine('fnB', 'fnC') } action.add(myActions)只要
myActions是合法的数据结构,.add()就能正常使用。 -
顺着上面的思路进一步拓展,我们甚至可以不用操心动作列表的产生。我们可以预先定一些原子操作(比如这里的
fnA、fnB和fnC),然后在 HTML 层面通过一定的命名约定来书写动作名。比如:<button data-action="a+c">Btn A</button> <button data-action="b+c">Btn B</button>然后使用一个构建工具来扫描 HTML,自动生成以下结构并传递给
action.add():{ 'a+c': ActionComposer.combine('fnA', 'fnC'), 'b+c': ActionComposer.combine('fnB', 'fnC') }这样连定义动作这一步都可以交给程序来自动完成了。
总之,我们的思路就是把 Action 当作一种底层的基础设施来用——上层的业务逻辑可以很复杂,但底层可以很简单!
为避免可能存在的歧义,从 v0.4 开始,action.add() 更名为 action.define()。
原 action.add() 仍可使用,但已不建议。