事件相关的优化
大部分的事件触发依赖于与浏览器的交互,但的行为是不可控的,许多交互设计上的缺陷与无法考虑到的因素会导致事件的频繁触发。
当事件处理器内部包含大量的操作,又不需要如此的响应事件时,就需要采用一些手段来限制事件处理器的执行。
事件的优化主要有两个目的:
通过交互的设计来优化事件是最常用到的方式。
如点击后将按钮。
<style>
.list .item {display: flex;justify-content: space-between;border-bottom: px dashed #4caf50;padding: px ;}
.list .item .caption {font-weight: ;}
.list .item .operates .delete {border: px solid rgb(, , );color: rgb(, , );outline: none;cursor: pointer;}
</style>
<div class="list">
<div class="item">
<div class="content caption">
今天的事情
</div>
<div class="operates caption">
操作
</div>
</div>
<div class="item">
<div class="content">
吃火锅
</div>
<div class="operates">
<button class="delete"></button>
</div>
</div>
<div class="item">
<div class="content">
和小姐姐聊天
</div>
<div class="operates">
<button class="delete"></button>
</div>
</div>
</div>
<script>
var listEle = document.querySelector('.list');
var deleteEle = document.querySelectorAll('.delete');
deleteEle.forEach(function(el) {
el.addEventListener('click', function() {
console.log('开始...');
setTimeout(function() {
var itemEl = el.parentNode.parentNode;
listEle.removeChild(itemEl);
console.log('成功');
}, );
});
});
</script>
上述例子没有在第一次点击后,对按钮做,或者采用一些上锁操作。
这种情况下可能会点击多次,操作通常会发送请求到服务端做处理,不对交互做优化可能会服务端的压力。
通过给予按钮状态就可以改善这个情况。
<style>
.list .item {display: flex;justify-content: space-between;border-bottom: px dashed #4caf50;padding: px ;}
.list .item .caption {font-weight: ;}
.list .item .operates .delete {border: px solid rgb(, , );color: rgb(, , );outline: none;cursor: pointer;}
</style>
<div class="list">
<div class="item">
<div class="content caption">
今天的事情
</div>
<div class="operates caption">
操作
</div>
</div>
<div class="item">
<div class="content">
吃火锅
</div>
<div class="operates">
<button class="delete"></button>
</div>
</div>
<div class="item">
<div class="content">
和小姐姐聊天
</div>
<div class="operates">
<button class="delete"></button>
</div>
</div>
</div>
<script>
var listEle = document.querySelector('.list');
var deleteEle = document.querySelectorAll('.delete');
deleteEle.forEach(function(el) {
el.addEventListener('click', function() {
console.log('开始...');
el.setAttribute('disabled', 'disabled');
el.style.color = 'rgb(226, 174, 174)';
el.style.borderColor = 'rgb(226, 174, 174)';
el.style.cursor = 'wait';
el.innerHTML = '处理中...';
setTimeout(function() {
var itemEl = el.parentNode.parentNode;
listEle.removeChild(itemEl);
console.log('成功');
}, );
});
});
</script>
在第一次点击按钮后,就给予按钮点击的状态,同时通过样式区分给予反馈,在提高体验的同时,优化了整个事件。
事件委托是利用事件冒泡的特性实现的,事件委托也被称为事件代理。
通过字面意思就可以理解,子节点的事件交给父节点来执行,一旦父节点发现子节点触发了对应的事件,就执行对应的事件处理器。
如:当点击按钮的时候,列表上的项
<style>
.list .item {display: flex;justify-content: space-between;border-bottom: px dashed #4caf50;padding: px ;}
.list .item .caption {font-weight: ;}
.list .item .operates .delete {border: px solid rgb(, , );color: rgb(, , );outline: none;cursor: pointer;}
</style>
<div class="list">
<div class="item">
<div class="content caption">
今天的事情
</div>
<div class="operates caption">
操作
</div>
</div>
<div class="item">
<div class="content">
吃火锅
</div>
<div class="operates">
<button class="delete"></button>
</div>
</div>
<div class="item">
<div class="content">
和小姐姐聊天
</div>
<div class="operates">
<button class="delete"></button>
</div>
</div>
</div>
<script>
var listEle = document.querySelector('.list');
listEle.addEventListener('click', function(e) {
var el = e.target;
if (el.className === 'delete') {
console.log('开始...');
el.setAttribute('disabled', 'disabled');
el.style.color = 'rgb(226, 174, 174)';
el.style.borderColor = 'rgb(226, 174, 174)';
el.style.cursor = 'wait';
el.innerHTML = '处理中...';
setTimeout(function() {
var itemEl = el.parentNode.parentNode;
listEle.removeChild(itemEl);
console.log('成功');
}, );
}
});
</script>
和上个小节对比,其实是一样的,但是这份示例中只在 .list
节点上绑定了事件,而上个小节则给每个按钮绑定了事件。
其关键的就是事件对象下的 target
,该表示当前事件流最终捕获到的元素。
很明显,根据 HTML 结构,按钮就是那一分支中能捕获到的最终节点。
当事件流到达捕获阶段后,则开始向上冒泡,进入冒泡阶段,在冒泡阶段会执行绑定在 .list
上的点击事件,在事件中对事件对象的 target
进行判定,如何条件就会执行真正想做的事情,这就是事件委托的流程。
事件委托
非常适合列表相关的事件处理,假设有成千上万条的列表,这个时候每个列表的操作按钮都要绑定事件,这个消耗是非常巨大的,当列表增减还需要考虑给新列表绑定事件,给的列表注销事件,这个时候使用事件委托,只需要在列表之外的节点上绑定事件,其好处而喻。
<style>
.list .item {display: flex;justify-content: space-between;border-bottom: px dashed #4caf50;padding: px ;}
.list .item .caption {font-weight: ;}
.list .item .operates .delete {border: px solid rgb(, , );color: rgb(, , );outline: none;cursor: pointer;}
.add { border: px dashed #4caf50; font-size: px; padding: px px; margin-top: px; outline: none; cursor: pointer; } .add:active { color: white; background: #4caf50; }
</style>
<div class="list">
<div class="item">
<div class="content caption">
今天的事情
</div>
<div class="operates caption">
操作
</div>
</div>
<div class="item">
<div class="content">
吃火锅
</div>
<div class="operates">
<button class="delete"></button>
</div>
</div>
<div class="item">
<div class="content">
和小姐姐聊天
</div>
<div class="operates">
<button class="delete"></button>
</div>
</div>
</div>
<button class="add">一项</button>
<script>
var listEle = document.querySelector('.list');
var deleteEle = document.querySelectorAll('.delete');
deleteEle.forEach(function(el) {
el.addEventListener('click', function() {
console.log('开始...');
el.setAttribute('disabled', 'disabled');
el.style.color = 'rgb(226, 174, 174)';
el.style.borderColor = 'rgb(226, 174, 174)';
el.style.cursor = 'wait';
el.innerHTML = '处理中...';
setTimeout(function() {
var itemEl = el.parentNode.parentNode;
listEle.removeChild(itemEl);
console.log('成功');
}, );
});
});
document.querySelector('.add').addEventListener('click', function() {
var el = document.createElement('div');
el.className = 'item';
el.innerHTML = [
'<div class="content">',
'学习',
'</div>',
'<div class="operates">',
'<button class="delete"></button>',
'</div>',
].join('');
listEle.appendChild(el);
});
</script>
稍微改写一下之前的例子,不采用事件委托的方式,这个列表中新增的项点击按钮是无用的,将这个例子改成事件委托的方式:
<style>
.list .item {display: flex;justify-content: space-between;border-bottom: px dashed #4caf50;padding: px ;}
.list .item .caption {font-weight: ;}
.list .item .operates .delete {border: px solid rgb(, , );color: rgb(, , );outline: none;cursor: pointer;}
.add { border: px dashed #4caf50; font-size: px; padding: px px; margin-top: px; outline: none; cursor: pointer; } .add:active { color: white; background: #4caf50; }
</style>
<div class="list">
<div class="item">
<div class="content caption">
今天的事情
</div>
<div class="operates caption">
操作
</div>
</div>
<div class="item">
<div class="content">
吃火锅
</div>
<div class="operates">
<button class="delete"></button>
</div>
</div>
<div class="item">
<div class="content">
和小姐姐聊天
</div>
<div class="operates">
<button class="delete"></button>
</div>
</div>
</div>
<button class="add">一项</button>
<script>
var listEle = document.querySelector('.list');
listEle.addEventListener('click', function(e) {
if (e.target.className === 'delete') {
var el = e.target;
console.log('开始...');
el.setAttribute('disabled', 'disabled');
el.style.color = 'rgb(226, 174, 174)';
el.style.borderColor = 'rgb(226, 174, 174)';
el.style.cursor = 'wait';
el.innerHTML = '处理中...';
setTimeout(function() {
var itemEl = el.parentNode.parentNode;
listEle.removeChild(itemEl);
console.log('成功');
}, );
}
});
document.querySelector('.add').addEventListener('click', function() {
var el = document.createElement('div');
el.className = 'item';
el.innerHTML = [
'<div class="content">',
'学习',
'</div>',
'<div class="operates">',
'<button class="delete"></button>',
'</div>',
].join('');
listEle.appendChild(el);
});
</script>
新增的项目是不需要再重新绑定事件的。
事件节流用于控制事件触发的最小间隔。
如事件 100 毫秒内只能触发一次。
如窗口缩放过程中对的元素大小重新调整,因为 resize
事件的触发是非常快的,虽然在频繁的变更窗口尺寸,但单位时间内能感知到的事情是有限的,也许一秒内执行100次尺寸计算和一秒钟内执行10次尺寸计算,感知上是没有太大区别的,而且事件内有太多的操作,又在频繁触发事件,这样很容易造成浏览器的卡顿。
<style>
.outer { position: fixed; top: ; left: ; right: ; bottom: ; background:rgb(, , ); }
.outer .text { position: absolute; top: ; left: ; transform: translate(-, -); color: white; font-size: px; text-shadow: px #fff, px #fff, px #fff, px #FF1177, px #FF1177, px #FF1177, px #FF1177, px #FF1177; }
</style>
<div class="outer">
<div class="text">100x200</div>
</div>
<script>
var text = document.querySelector('.text');
var resize = function() {
var height = window.innerHeight;
var width = window.innerWidth;
text.innerText = width + 'x' + height;
};
window.addEventListener('resize', resize);
resize();
</script>
可以看到,resize 事件的响应是非常快的,与之类似的还有 scroll
事件,即滚动条滚动时触发的事件。
这种情况下就可以使用节流的方式来优化事件。
<style>
.outer { position: fixed; top: ; left: ; right: ; bottom: ; background:rgb(, , ); }
.outer .text { position: absolute; top: ; left: ; transform: translate(-, -); color: white; font-size: px; text-shadow: px #fff, px #fff, px #fff, px #FF1177, px #FF1177, px #FF1177, px #FF1177, px #FF1177; }
</style>
<div class="outer">
<div class="text"></div>
</div>
<script>
var text = document.querySelector('.text');
var timer = false;
var resize = function() {
if (timer) return; // 判断是不是上一次事件执行完300毫秒内
var height = window.innerHeight;
var width = window.innerWidth;
text.innerText = width + 'x' + height;
timer = setTimeout(function () {
timer = null;
}, );
};
window.addEventListener('resize', resize);
resize();
</script>
对例子做了简单的,了 timer
变量,用于存放定时器的标志值(定时器的返回值),每当事件触发时,给 timer 赋值,这个时候事件就会处于锁住的状态,直到 300 毫秒后,timer 再次被设置为 null,表示可以触发事件。
根据需求,业务逻辑执行的时机是在定时器内还是定时器外可以自由调配。
例如设定间隔时间为 200 毫秒,防抖则是在事件触发后 200 毫秒再执行事件处理器。
假设200毫秒内又触发了相同事件,则取消上一次的事件,不执行事件处理器,以最后一次触发事件的时机开始,等待 200 毫秒执行事件处理器。
这种情况常用于输入,如表单验证,关键词联想。
引擎基本都含有关键词联想,即在输入关键词的时候,根据关键词联想相关,为更好的命中。
<style>
input { border: px solid #eee; padding: px px; min-width: px; font-size: px; height: px; display: block; margin: auto; outline: none; }
.result { text-align: center; }
</style>
<div>
<input type="text">
<div class="result"></div>
</div>
<script>
var input = document.querySelector('input');
var result = document.querySelector('.result');
input.addEventListener('input', function(e) {
var val = e.target.value;
result.innerText = '联想:' + val;
});
</script>
假设的就是服务端返回的联想。
其实可以发现,在输入关键词的时候,基本是按照词组的方式进行的,在某词组输入完成前去联想其实意义不大。
如网官方网站,可能会先连续打出网
,而对于事件而言,就触发了许多次,通过防抖就可以剔除这些无意义的事件触发。
<style>
input { border: px solid #eee; padding: px px; min-width: px; font-size: px; height: px; display: block; margin: auto; outline: none; }
.result { text-align: center; }
</style>
<div>
<input type="text">
<div class="result"></div>
</div>
<script>
var input = document.querySelector('input');
var result = document.querySelector('.result');
var timer = null;
input.addEventListener('input', function(e) {
clearTimeout(timer);
timer = setTimeout(function() {
var val = e.target.value;
result.innerText = '联想:' + val;
}, );
});
</script>
通过定时器,来延迟执行事件处理器,每次触发事件,就取消上一次事件处理器,这样就达到了防抖的。
这个方案目前使用的比较少,其就是在事件被触发的时候,去加载远端的事件处理器,加载完毕后再执行事件处理器。
以前因为缺少模块化规范,基本看不到这种优化方案,现在因为新标准动态import
的出现,使其非常容易融合进业务中。
目前有许多构建工具动态的 import
,利用构建工具可以非常简单的实现异步加载事件处理器。
// 这是一份伪
const el = document.querySelector('.delete');
el.addEventListener('click', async () => {
try {
const event = await import('./event/delete.js');
// ...
} catch (e) {
// ...
}
});
这么做其实优化的并不是事件本身,主要是为了减少首屏加载的体积。
事件的优化不一定要只从方面入手,还可以从其他方面,如交互上进行思考。