本篇课题,或许早已是烂大街的解读文章。不过春招系列面试下来,不少伙伴们还是似懂非懂地栽倒在(~面试官~)深意的笑容之下,权当温故知新。
JavaScript 的执行过程,是基于栈来进行的。复杂的程序代码被封装到函数中,程序执行时,函数不断被推入执行栈中。所以 “ 执行栈 ” 也称 “ 函数执行栈 ”。
函数中封装的代码块,一般都有相对复杂的逻辑处理(计算 / 判断),例如函数中可能会涉及到 DOM 的渲染更新,复杂的计算与验证,Ajax 数据请求等等。
前端页面的操作权,大部分都是属于浏览断的客户爸爸们(单身三十年的手速,惹不起惹不起!!!)。如果函数被频繁调用,造成的性能开销绝对不只一点点。
前:DOM 频繁重绘的卡顿让客户爸爸们想把你揪出来一顿大招。。。
后:后端同学正在提刀赶来的路上:“为什么我的接口被你玩挂了”。。。
既要提升用户体验,又要减少后端服务开销,可见我们大前端的使命不只一页 PPT。说好前因,接着就是后果了。既然有优化的需求,必然就要有相应的解决方案。隆重请出主角:“防抖”与“节流”。
防抖(debounce)
在事件被触发 n 秒后再执行回调函数,如果在这 n 秒内又被触发,则重新计时延迟时间。
生活化理解:英雄的技能条,技能条读完才能使用技能(R 大招 60s)
防抖的实现方式分两种“立即执行”和“非立即执行”,区别在于第一次触发时,是否立即执行回调函数。
非立即执行
”非立即执行防抖“指事件触发后,回调函数不会立即执行,会在延迟时间 n 秒后执行,如果 n 秒内被调用多次,则重新计时延迟时间
// e.g. 防抖 – 非立即执行
function debounce(func, delay) {
var timeout;
return function() {
var context = this;
var args = arguments;
// && 短路运算 == if(timeout) else {…}
timeout && clearTimeout(timeout);
timeout = setTimeout(function(){
func.apply(context, args);
}, delay);
}
}
// 调用
var printUserName = debounce(function(){
console.log(this.value);
}, 800);
document.getElementById(‘username’)
.addEventListener(‘keyup’, printUserName);
立即执行
“立即执行防抖”指事件触发后,回调函数会立即执行,之后要想触发执行回调函数,需等待 n 秒延迟
// e.g. 防抖 – 立即执行
function debounce(func, delay) {
var timeout;
return function() {
var context = this;
var args = arguments;
callNow = !timeout;
timeout = setTimeout(function() {
timeout = null;
}, delay);
callNow && func.apply(context, args);
}
}
函数防抖原理:通过维护一个定时器,其延迟计时以最后一次触发为计时起点,到达延迟时间后才会触发函数执行。
节流(throttle)
规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效(间隔执行)
生活化理解:
FPS 射击游戏子弹射速(即使按住鼠标左键,射出子弹的速度也是限定的)
水龙头的滴水(水滴攒到一定重量才会下落)
函数节流实现的方式有“时间戳”和“定时器”两种。
时间戳
// e.g. 节流 – 时间戳
function throttle(func, delay) {
var lastTime = 0;
return function() {
var context = this;
var args = arguments;
var nowTime = +new Date();
if (nowTime > lastTime + delay) {
func.apply(context, args)
lastTime = nowTime;
}
}
}
“时间戳”的方式,函数在时间段开始时执行。
缺点:假定函数间隔 1s 执行,如果最后一次停止触发,卡在 4.2s,则不会再执行。
定时器
// e.g. 节流 – 定时器
function throttle(func, delay) {
var timeout;
return function() {
var context = this;
var args = arguments;
if (!timeout) {
setTimeout(function(){
func.apply(context, args);
timeout = null;
}, delay)
}
}
}
“定时器”的方式,函数在时间段结束时执行。可理解为函数并不会立即执行,而是等待延迟计时完成才执行。(由于定时器延时,最后一次触发后,可能会再执行一次回调函数)
时间戳 + 定时器(互补优化)
// e.g. 节流 – 时间戳 + 定时器
function throttle(func, delay) {
let lastTime, timeout;
return function() {
let context = this;
let args = arguments;
let nowTime = +new Date();
if (lastTime && nowTime < lastTime + delay) {
timeout && clearTimeout(timeout);
timeout = setTimeout(function(){
lastTime = nowTime;
func.apply(context, args);
}, delay);
} else {
lastTime = nowTime;
func.apply(context, args);
}
}
}
合并优化的原理:“时间戳”方式让函数在时间段开始时执行(第一次触发立即执行),“定时器”方式让函数在最后一次事件触发后(如 4.2s)也能触发。
函数节流原理:一定时间内只触发一次,间隔执行。通过判断是否到达指定触发时间,间隔时间固定。
“防抖”与“节流”的异同
相同:都是防止某一时间段内,函数被频繁调用执行,通过时间频率控制,减少回调函数执行次数,来实现相关性能优化。
区别:“防抖”是某一时间内只执行一次,最后一次触发后过段时间执行,而“节流”则是间隔时间执行,间隔时间固定。
“防抖”与“节流”的应用场景
防抖
文本输入搜索联想
文本输入验证(包括 Ajax 后端验证)
节流
鼠标点击
监听滚动 scroll
窗口 resize
mousemove 拖拽
应用场景还有很多,具体场景需具体分析。只要涉及高频的函数调用,都可参考函数防抖节流的优化方案。
鼓起勇气写在结尾:以上代码都不是“完美”的“防抖 / 节流”实现代码!!!仅就实现方式和基本原理,浅谈分解一二。
实际代码开发中,一般会引入 lodash 相对“靠谱”的第三方库,帮我们去实现防抖节流的工具函数。有兴趣的伙伴们可阅读 lodash 相关源码,加深印象理解可再读以下参考文章。
参考文章
7 分钟理解 JS 的节流、防抖及使用场景
函数防抖和节流