最近常在移动端开发,由于不是大型站点,不需要使用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,让浏览器回收内存。
此外,一次性事件的实现很简单,就是执行一次后解绑事件即可。
整理代码后,之后的绑定操作就不用干重复活咯~