原生js事件绑定和事件委托

最近常在移动端开发,由于不是大型站点,不需要使用vue等库,也不需要引用jquery和zepto等。

写得最多得就是各种元素选择器和事件绑定,操作下来也是有点烦人。这时候怀念起jquery的美好来了,但是仅为了这个引用这么大个库,实在时下不了手,思考了一下,直接在html构造函数上拓展支持了。

按照习惯来走,一般我们会喜欢在选择的元素上直接 on + 事件 ,加上相应的逻辑函数完成一个事件绑定的。

这里要说明一下,js的事件绑定是特殊的,不同于老旧版本的事件绑定

element.onclick = function () {}

这种方式绑定的事件,后面有其他相同绑定事件,会覆盖前面的逻辑代码,因此我们会使用 addEventListener 实现绑定,按照绑定事件的顺序执行所有操作。

element.addEventListener('click', function() {
    console.log(1)
}, false)

element.addEventListener('click', function() {
    console.log(2)
}, false)

>> 1
>> 2

再深一步思考一下,什么是 事件委托 ,意义是什么。

事件委托,实际是把目标元素的事件绑定到其他元素上,借助触发事件。举个例子,我需要做个div容器展示文章,容器有个按钮,需要点击放大字体。这里你可以考虑把事件绑定到按钮上,也可以绑定到div上,两者实现效果并无不同。

但是要记得一点,所有的元素查找和事件绑定都是需要消耗性能和内存的,而复杂页面上的操作区域可能多达数十上百,每一个都独立绑定事件,页面在低端设备上也会出现执行卡顿的情况,影响体验。此外,常见需求里是有未来事件绑定的,你无法为尚未存在的元素绑定事件。

原生js实现委托一般是从event属性中获取当前点击位置的元素,并对元素进行逻辑代码。因此,可以考虑将两者做到一起。

/** 
 * 事件绑定和事件委托
 * 类似jQuery的on/off/one的用法
 * @author mo
 * @param eventName 事件名称
 * @param selector 委托的子元素,可以为空
 * @param callback 事件逻辑代码
 * @description 在选择的元素上直接用on/off/one就好了,同时存在自身绑定事件和委托事件的,如需解绑,要分别解绑,仅off选择元素的事件是不会解绑代理的事件的
 * @example
 *
 * var test = document.querySelector('.test')
 *
 * test.on('click', function () {
 *    // TODO 
 * })
 *
 * test.one('click', function () {
 *    // TODO 
 * })
 *
 * test.on('click', '.childElement', function () {
 *    // TODO 
 * })
 *
 * test.off('click')
 *
 * test.off('click', '.childElement')
 *
 */

var p_n_space = /(^\s*)|(\s*$)/g, // 前后空格正则
    bind_list = {} // 绑定事件列表

HTMLElement.prototype.on = function(eventName, selector, callback) {

    // 预处理参数
    if (!eventName || !selector) {
        console.log('Arguments is require!')
        return false
    } else {

        eventName = eventName.toLowerCase()

        if (typeof selector == 'function') {
            callback = selector
            selector = null
        }
    }

    // 事件绑定逻辑

    if (!bind_list.eventName) bind_list.eventName = []

    bind_list.eventName.push({
        selector: selector,
        fn: function(event) {

            var ev = event || window.event,
                target = ev.target || ev.srcElement,
                targets, sSets, i = j = 0

            if (!selector) {

                // 当前元素绑定
                callback.apply(this, [ev])

                if (this.once) {
                    delete this.once
                    this.off(eventName, callback)
                }

            } else {

                targets = selector.split(',')

                for (; i < targets.length; i++) {

                    // 删除前后空格
                    targets[i] = targets[i].replace(p_n_space, '')

                    // 遍历集合
                    sSets = this.querySelectorAll(targets[i])

                    // 如果集合为空则说明不存在这种委托元素,不做处理
                    if (sSets.length) {
                        // 关系拆分
                        // targets[i] = targets[i].split(/\s+/g).reverse()

                        // 事件委托
                        for (var j = 0; j < sSets.length; j++) {
                            if (target === sSets[j]) {

                                callback.apply(target, [ev])

                                if (this.once) {
                                    delete this.once
                                    this.off(eventName, selector, callback)
                                }

                                break;
                            }
                        }

                    } else {
                        return false
                    }

                }

            }
        }
    })

    // 所有事件,包括委托事件都绑定到目标元素本身
    if (this.addEventListener) {
        this.addEventListener(eventName, bind_list.eventName[bind_list.eventName.length - 1].fn, false);
    } else if (this.attachEvent) {
        this.attachEvent("on" + eventName, bind_list.eventName[bind_list.eventName.length - 1].fn);
    }

}

// 移除全部事件
HTMLElement.prototype.off = function(eventName, selector) {

    if (!selector) selector = null

    // 预处理参数
    if (!eventName) {
        console.log('Arguments is require!')
        return false
    } else {

        eventName = eventName.toLowerCase()

    }


    // 遍历已添加列表
    for (var k = 0; k < bind_list.eventName.length; k++) {
        // 仅移除相关的事件,分目标元素和委托元素绑定的事件
        if (bind_list.eventName[k] && selector == bind_list.eventName[k].selector) {
            if (this.removeEventListener) {
                this.removeEventListener(eventName, bind_list.eventName[k].fn, false);
            } else if (this.detachEvent) {
                this.detachEvent("on" + eventName, bind_list.eventName[k].fn);
            }
        }

        // 移除
        bind_list.eventName[k] = null
    }


}

// 一次性事件
HTMLElement.prototype.one = function(eventName, selector, callback) {
    this.once = true
    this.on(eventName, selector, callback)
}

需要注意,匿名function,即使写得一模一样,但是实际也是两个不同的对象。而原生js解绑事件时是需要传入相同的函数对象的,因此要具名声明函数,为了方便管理,我全放在了一个统一变量中保存,不需要时指向于null,让浏览器回收内存。

此外,一次性事件的实现很简单,就是执行一次后解绑事件即可。

整理代码后,之后的绑定操作就不用干重复活咯~

你可能感兴趣的:(原生js事件绑定和事件委托)