本篇课题, 或许早已是烂大街的解读文章. 不过春招系列面试下来, 不少伙伴们还是似懂非懂地栽倒在 (~面试官~) 深意的笑容之下, 权当温故知新.
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 的节流, 防抖及使用场景 https://juejin.im/post/5b8de829f265da43623c4261
函数防抖和节流 https://juejin.im/post/5b651dc15188251aa30c8669
来源: https://segmentfault.com/a/1190000018383955