Fork me on GitHub

一个点击事件引发的案件

注:本文非本人所写,参考别家。

引言

前端之所以被称为前端,是因为它是整个web技术栈中距离用户最近,直接与用户进行交互的一环。而网页界面与用户的交互通常是通过各种事件来达成的;在点击事件中,点击事件又是一种最常见,最常用的一种界面事件。

本文将介绍我在“点击事件绑定”这一事件中的进阶之路。

背景

我是一个前端小兵,我在一家互联网公司做一些简单的业务开发。

某一天,我接到了一个需求,做一个抽奖功能。公司里的前辈们已经完成了业务逻辑,而且已经提供了业务功能的借口,只需要我制作页面并完成时间绑定即可。

 

实践

开动

我写好了页面,页面中有一个ID为 lucky-draw的按钮元素。接下来,我需要为他绑定事件。我是这样写的:

var btn = document.getElementById("lucky-draw");

btn.onclick = function (){

  BX.luckyDraw();

}

这其中 BX是前辈们提供的业务接口,执行它久可以进行抽奖的后续功能。

 

第一关

然而前辈告诉我,这些重要功能的按钮是需要加统计。这也难不倒我,因为我很熟悉统计的

API。于是我修改了一下事件绑定时的代码:

btn.onclick = function (){

  BX.luckyDraw();

  BX.track('lucky-draw');

}

这样做是有效的,但是前辈们告诉我,因为某些原因,统计代码和业务代码是分布在不同位置的,以上代码需要拆开。于是我尝试修改:

btn.onclick = function (){BX.luckyDraw()}

btn.onclick = function (){BX.trackd('lucky-draw')}

结果发现点击按钮时抽奖功能失效 了。原来,使用.onclick这样的事件属性有一个非常大的缺点,重复赋值会覆盖原来的值。也就是说,这种方式只能绑定最后赋值的事件处理函数。

我硬着头皮去请教前别,发现这种方式早就不推荐使用了,应该使用DOM标准的事件绑定api来处理。因为代码改成这样:

btn.addEventListener('click', function (){

  BX.luckyDraw();

}, false)

//other codes...

btn.addEventListener('click', function (){

  BX.track('lucky-draw');

})

所有功能终于正常了,我有准备开心的上线了。

第二关

实时证明,我还是太天真了,PM是不会一次性把所有需求都告诉你的。原来,这个抽奖功能还需要做A/B测试,也就是说只有一半的用户会看到这个功能。

这意味着用户的页面上可能根本没有btn这个元素,那么btn.addEventListener()这一句直接就抛错了。因此,在为按钮绑定事件处理之前,我不得不先判断一下:

if(btn){

 btn.addEventListern('click', function (){

  BX.luckyDraw();

 },false);

}

if(btn){

 btn.addEventListern('click', function (){

  BX.track('lucky-draw');

 },false);

}

 

虽然这个代码在所有用户的界面上都能正常工作,但这些预先判断看起来略显蛋疼。我再次带着疑惑向前辈请教。前辈慈祥的看着我,说出了一句经典名言:

傻瓜,为什么不用万能的jQuery呢?

原来,神奇的jQuery允许我们忽略很多细节,比如这种没有取到元素的情况被它默默的消化掉。而且jQuery的事件绑定方法也不存在兼容性问题,api也比较好看。不错不错,不管网上的大神们怎么喷jQuery,但它简直是我的救星啊!

于是代码变成了这样:

var $btn = $('lucky-draw');

$btn.on('click', function (){

  BX.luckyDraw();

})

$btn.on('click', function (){

     BX.track('lucky-draw');

})

我的代码看起来像回事了,我准备开始上线

第三关

当然我的故事不会这么快结束。要知道,对一个有追求的前端来说,不断提升用户体验是一个永恒的目标。比如,我们网站用一些方法来提升网页加载性能,部分网页内容并不是原来存在于页面中的,而是用户需要时,有javascript生成的。

拿这个抽奖功能来说,抽奖按钮存在于一个名为“惊喜”的tab中,而这个tab在初始化状态中时没有内容的,只有当用户点击tab时,才会由js填充其内容,示意代码是这样的:

$('.tabs > .surprise').on('click', function () {

var htmlSurpriseTab = [

'<div>',

'<button id="lucky-draw">Lucky Draw</button>',

'</div>'

].join('')

$('.tab-panels > .surprise').html(htmlSurpriseTab)

 

// BTN READY

})

 

 

这意味着,我写的事件绑定代码需要写在 // BTN READY 处。这种深层的耦合看起来很不理想,我需要想办法解决它。

 

我想起来,我在阅读 jQuery 文档时看到有一种叫作 “事件委托” 的方法,可以在元素还未添加到页面之前就为它绑定事件。于是,我尝试这样来写:

 

$(document.body).on('click', '#lucky-draw', function () {

BX.luckyDraw()

})

 

果然,我成功了!好事多磨啊,这个需求终于开心地上线了。

 

经过进一步的研究,我了解到 “事件委托” 的本质是利用了事件冒泡的特性。把事件处理函数绑定到容器元素上,当容器内的元素触发事件时,就会冒泡到容器上。此时可以判断事件的源头是谁,再执行对应的事件处理函数。由于事件处理函数是绑定在容器元素上的,即使容器为空也没有关系;只要容器的内容添加进来,整个功能就是准备就绪的。

虽然事件委托的原理听起来稍有些复杂,但由于 jQuery 对事件委托提供了完善的支持,我的代码并没有因此变得很复杂。

 

经过这一番磨炼,我收获了很多经验值;同时,我也学会了更进一步去发现问题和思考问题。比如,在我们的网页,通常会有多个按钮,那为它们绑定事件的脚本代码可能就是这样的:

 

$body = $(document.body)

$body.on('click', '#lucky-draw', function () {

BX.luckyDraw()

})

 

$body.on('click', '#some-btn', function () {

// do something...

})

$body.on('click', '#another-btn', function () {

// do something else...

})

 

我隐隐觉得这样不对劲啊!虽然这些代码可以正常工作,但每多一个按钮就要为 body 元素多绑定一个事件处理函数;而且根据直觉,这样一段段长得差不多的代码是需要优化的。因此,如果我可以把这些类似的代码整合起来,那不论是在资源消耗方面,还是在代码组织方面,都是有益的。

 

于是,我尝试把所有这些事件委托的代码合并为一次绑定。首先,为了实现合并,我需要为这些按钮找到共同点。很自然地,我让它们具有相同的 class:

 

<button class="action" id="lucky-draw">Lucky Draw</button>

<button class="action" id="some-action">Button</button>

<a href="#" class="action" id="another-action">Link</a>

<a href="#" class="action" id="another-action-2">Link</a>

 

然后,我试图通过一次事件委托来处理所有这些按钮:

 

$body.on('click', '.action', function () {

// WHEN CLICK ANY '.action', WE COME HERE.

})

 

很显然,所有具有 action 类名的元素被点击后都会触发上面这个事件处理函数。那么,接下来,我们在这里区分一下事件源头,并执行对应的任务:

$body.on('click', '.action', function () {

switch (this.id) {

case 'lucky-draw':

BX.luckyDraw()

break

case 'some-btn':

// do something...

break

// ...

}

})

 

这样一来,所有分散的事件委托代码就被合并为一处了。在这个统一的事件处理函数中,我们使用 ID 来区分各个按钮。

 

但 ID 有一些问题,由于同一页面上不能存在同名的元素,相信前端工程师们都对 ID 比较敏感,在日常开发中都尽量避免滥用。此外,如果多个按钮需要执行的任务相同,但它的 ID 又必须不同,则这些 ID 和它们对应的任务之间的对应关系就显得不够明确了。

 

于是,我改用 HTML5 的自定义属性来标记各个按钮:


<button class="action" data-action="lucky-draw">Lucky Draw</button>

<button class="action" data-action="some-action">Button</button>

<a href="#" class="action" data-action="another-action">Link</a>

<a href="#" class="action" data-action="another-action-2">Link</a>

 

我在这里使用了 data-action 这个属性来标记各个按钮元素被点击时所要执行的动作。回过头看,由于各个按钮都使用了这个属性,它们已经具备了新的共同点,而 class 这个共同点就不必要了,于是我们的 HTML 代码可以简化一些:

 

<button data-action="lucky-draw">Lucky Draw</button>

<button data-action="some-action">Button</button>

<a href="#" data-action="another-action">Link</a>

<a href="#" data-action="another-action-2">Link</a>

 

同时 JS 代码也需要做相应调整:

 

$body.on('click', '[data-action]', function () {

var actionName = $(this).data('action')

switch (actionName) {

case 'lucky-draw':

BX.luckyDraw()

break

case 'some-btn':

// do something...

break

// ...

}

})

 

我们的代码看起来已经挺不错了,但我已经停不下来了,还要继续改进。那个长长的 switch 语句看起来有点臃肿。通常优化 switch 的方法就是使用对象的键名和键值来组织这种对应关系。于是我继续改:

var actionList = {

  'lucky-draw': funciton (){//do something}

  'some-btn': function (){//do something}

}

 

$body.on('click', '[data-action]', function (){

  var actionName = $(this).data('action');

  var action = actionList[actionName];

   if($.isFucntion(action)){action()}

})

经过这样的调整,我发现代码的嵌套变浅了,而且按钮们的标记和它们要做的事情也被组织成了actionList 这个对象,看起来更清爽了。

 

在这样的组织方式下,如果页面需要新增一个按钮,也很容易做扩展:

 

// HTML

$body.append('<a href="#" data-action="more-action">Link</a>')

 

// JS

$.extend(actionList, {

'more-action': function () {

// ...

}

})

 

到这里,这一整套实践终于像那么回事了!

 

posted @ 2015-10-13 11:13  donglegend  阅读(144)  评论(0编辑  收藏  举报