日志更新

如何在事件代理中正确使用 focus 和 blur 事件

什么是事件代理(Event Delegation)?

如果不太了解的朋友,可详细阅读:《Event delegation in JavaScript》,这里不再累述。

首先让我们一起来回顾一些常识:

  1. 通常支持事件冒泡(Event Bubbling)的事件类型为鼠标事件和键盘事件,例如:mouseover, mouseout, click, keydown, keypress
  2. 接口事件则通常不支持事件冒泡(Event Bubbling),例如:load, change, submit, focus, blur

很明显:focus 和 blur 都属于不支持冒泡的接口事件。既然都不支持冒泡,那又如何实现事件代理呢?

可以换个角度,逆向思维,尝试事件捕获(Event Capturing,除了IE,现在流行的标准浏览器均支持)。

测试后会发现,如果你捕获 focus 或 blur 事件,目标元素的祖先元素均执行事件函数。至于为什么?或许是实现的一个 BUG。

el.addEventListener('focus', focusHandler, true);
el.addEventListener('blur', blurHandler, true);

那对于 IE ,我们如何实现呢?

非常幸运,IE 下支持 focusin 和 focusout 事件,非常类似于 focus 和 blur 事件,唯一不同的是,这两种事件支持事件冒泡(Event Bubbling)。

el.onfocusin = focusHandler;
el.onfocusout = blurHandler;

很完美的解决方案:

if (document.addEventListener) {
    el.addEventListener('focus', focusHandler, true);
    el.addEventListener('blur', blurHandler, true);
} else {
    el.onfocusin = focusHandler;
    el.onfocusout = blurHandler;
}

当你下次看到 YUI 2.8 的 event/event-debug.js 源码中下面几段代码时,一定会清晰很多:

576    // String constants used by the addFocusListener and removeFocusListener methods
577
578     FOCUSIN = "focusin",
579     FOCUSOUT = "focusout";

        ....

750     _specialTypes: {
751         focusin: (isIE ? "focusin" : "focus"),
752         focusout: (isIE ? "focusout" : "blur")
753      },

        ....

1053    addListener: function (el, sType, fn, obj, overrideContext) {
1054
1055	    var capture = ((sType == FOCUSIN || sType == FOCUSOUT) && !YAHOO.env.ua.ie) ? true : false;
1056
1057        return this._addListener(el, this._getType(sType), fn, obj, overrideContext, capture);
1058
1059    },

修复 jQuery 中 isFunction 方法的 BUG

jQuery 1.4 源码 449 行(core.js 431 行),判断是否为函数的方法如下(思路来源于 Douglas Crockford 的《The Miller Device》):

isFunction: function( obj ) {
    return toString.call(obj) === "[object Function]";
},

同时 jQuery 的作者也作了部分注释:

See test/unit/core.js for details concerning isFunction. Since version 1.3, DOM methods and functions like alert aren’t supported. They return false on IE (#2968).

即:此方法在 IE 下无法正确识别 DOM 方法和一些函数(例如 alert 方法等)。

为什么会这样呢?

详细看测试页面:http://www.planabc.net/demo/isfunction/

会发现在 IE 下用 typeof 检测 alert、confirm 方法以及 DOM 的方法显示 object,而其他浏览器下显示 function。

那如何完善这个问题呢?

  1. typeof 检测某个方法(例如:document.getElementById) 是否是 object,如何是,则重写 isFunction 函数;
  2. 怎样重写呢?正则判断传入的对象字符串后(”" + fn),是否起始位置含有 function,即:/^\s*\bfunction\b/.test(” + fn)。

OK,看下根据以上思路修改后的 isFunction 函数:

var isFunction = (function() {
    // Performance optimization: Lazy Function Definition
    return "object"  === typeof document.getElementById ?
           isFunction = function(fn){
                try {
                    return /^\s*\bfunction\b/.test("" + fn);
                } catch (x) {
                    return false
                }
           }:
           isFunction = function(fn){
               return "[object Function]" === Object.prototype.toString.call(fn);
           };
})()

参考阅读:

如何将函数的实际参数转换成数组

实际参数在函数中我们可以使用 arguments 对象获得 (注:形参可通过 arguments.callee 获得),虽然 arguments 对象与数组形似,但仍不是真正意义上的数组。

值得庆幸的是,我们可以通过数组的 slice 方法将 arguments 对象转换成真正的数组:

var args = Array.prototype.slice.call(arguments, 0);

对于slice 方法,ECMAScript 262 中 15.4.4.10 Array.prototype.slice (start, end) 章节有备注:

The slice function is intentionally generic; it does not require that its this value be an Array object. Therefore it can be transferred to other kinds of objects for use as a method. Whether the slice function can be applied successfully to a host object is implementation-dependent.

《Pro JavaScript Design Patterns》(《JavaScript 设计模式》)的作者 Dustin Diaz 曾指出:

instead of…
var args = Array.prototype.slice.call(arguments, 0); // 怿飞注:下称方法一
do this…
var args = [].slice.call(arguments, 0); // 怿飞注:下称方法二

但二者的性能差异真的存在吗?经过个人简单测试发现:

arguments.length 较小的时候,方法二性能上稍有一点点优势,而在arguments.length 较大的时候,方法一却又稍有优势。

2010年1月30日更新(测试地址):几经验证,性能差异不大,反而第一张方法性能稍优势一点,或许由于第二种方法创建新数组产生开销。

最后附上方法三,最老土的方式:

var args = [];
for (var i = 1; i < arguments.length; i++) {
    args.push(arguments[i]);
}

不过对于平常来说,个人建议使用第一种方法,但任何解决方案,没有最好的,只有最合适:

var args = Array.prototype.slice.call(arguments, 0); 

------------------------------------------------------------------

如何将 NodeList (比如:document.getElementsByTagName('div'))转换成数组呢?

解决方案简单如下:

function nodeListToArray(nodes){
    var arr, length;

    try {
        // works in every browser except IE
        arr = [].slice.call(nodes);
        return arr;
    } catch(err){
        // slower, but works in IE
        arr = [];
        length = nodes.length;

        for(var i = 0; i < length; i++){
             arr.push(nodes[i]);
         }  

        return arr;
    }
} 

为什么 IE 中 NodeList 不可以使用 [].slice.call(nodes) 方法转换呢?

In Internet Explorer it throws an error that it can't run Array.prototype.slice.call(nodes) because a DOM NodeList is not a JavaScript object.

删除数组中重复项(uniq)

首先让我们看下 YUI 是如何处理的:

var toObject = function(a) {
    var o = {};
    for (var i = 0; i < a.length; i = i+1) {
     o[a[i]] = true;
    }
    return o;
};

var keys = function(o) {
    var a=[], i;
    for (i in o) {
        if (lang.hasOwnProperty(o, i)) { // YUI的方法
            a.push(i);
        }
    }
    return a;
};

var uniq = function(a) {
    return keys(toObject(a));
};

详细分析,见同事 长天 的分享 《巧妙去除数组中的重复项》

自己使用的方式与 YUI 的方式十分相似,不过仅使用了一次循环便完成了删除数组中重复项,如下:

var uniq = function (arr) {
    var a = [],
        o = {},
        i,
        v,
        len = arr.length;

    if (len < 2) {
        return arr;
    }

    for (i = 0; i < len; i++) {
        v = arr[i];
        if (o[v] !== 1) {
            a.push(v);
            o[v] = 1;
        }
    }

    return a;
}

经过了简单的测试:自己使用的方式性能远远高于 YUI 的方式。

抛砖引玉,欢迎大家提供更好的处理方法。

2009年12月28日更新:

以上两种函数方法暂时都不能处理复杂的含有混合类型的数组(感谢 小猫 提出的疑问),如:[0,"0",1,"1",0]、["null",null]。

对于能够约定类型为数字(注:要求非0开头的数字,小数除外)或字符串的数组,我们可以用改进后的函数方法(感谢 closurecache 提供的思路):

var uniq = function (arr) {
    var a = [],
        o = {},
        i,
        v,
        cv, // corrected value
        len = arr.length;

    if (len < 2) {
        return arr;
    }

    for (i = 0; i < len; i++) {
        v = arr[i];

        /* closurecache 提供的函数中使用的是  cv = v + 0;,
         * 这样就无法辨别类似[1, 10, "1", "10"]的数组,
         * 因为运算后 => 1, 10, 10, 100,很明显,出现了重复的标示符。
         * 加前面就难道没问题吗?
         * 有的:数组中不能出现类似01 、001,以 0 开头的数字,
         * 但适用性比原先更广。
         */
        cv = 0 + v;

        if (!o[cv]) {
            a.push(v);
            o[cv] = true;
        }
    }

    return a;
}

如果大家想在此解题思路的基础上,更完美一点,推荐 Dexter.Yy 的方法,进行类型判断,给予唯一标示符,详见 评论 20 楼

没有最好,只有最合适的方式,其实使用 Array.indexOf() 的思路也是不错的选择,对于已经支持的浏览器直接用原生的 Array.indexOf() 方法,对于未支持的,我们增加 Array.indexOf() 方法,如下:

if(!Array.prototype.indexOf) {
    Array.prototype.indexOf = function (obj, fromIndex) {
        if (fromIndex == null) {
            fromIndex = 0;
        } else if (fromIndex < 0) {
            fromIndex = Math.max(0, this.length + fromIndex);
        }

        for (var i = fromIndex; i < this.length; i++) {
            if (this[i] === obj)
                return i;
            }
        return -1;
    };
}

接下来,实现的过程就非常简单了。

对于使用 Array.indexOf() 方法实现方案的优化提示:找到相同值时,从数组中移除,以减少下次遍历的量。

第4届D2前端技术论坛开始报名

2005年前端工程师的职位开始在中国出现,2007年第一届D2前端技术论坛在杭州举行,从此D2伴随着中国的前端一起成长,从此越来越多的互联网产品的背后有着职业前端工程师坚定的身影。

在D2上我们曾交流过某项具体的前端技术,也曾探讨过团队协作的方式,那些美妙的话题都深入到我们频繁敲键盘的指尖。回首我们过去四年的坚持和梦想,现在是时候分享我们的积累和成果了。当架构、安全、性能等等在前端的领域扎根的时候,我们自己也在悄然发生质变,而推动这些变化的人和事正是D2想告诉你的。

最新评论

  • Robin: 自己尝试写了一个,最坏的时间复杂...
  • lianleif: 很受用,谢谢!学到一些东...
  • prcjack: 在加上 渔隐 所提醒的valueOf判断,应...
  • prcjack: 经测试,alert等window方法和getElementById...
  • rainoxu: 总是能在这里学到新东西:...
  • 元云: 偏好是自己的,“走自己的路,让别...
  • kangre: 怿飞的学习都很广泛哦!!学识渊博...
  • Tianchao: 您的评论正在穿越伟大的GFW,哈哈 ...
  • Tianchao: 太强了...
  • 怿飞: @闲耘 谢谢,原文的确有不准确地方...

管理登录