本篇文章参考书籍《JavaScript设计模式》–张容铭
前言
今年3月份的时候,写过几篇文章,,介绍了一下浏览器的回流,重绘,以及对应的解决办法分别是节流和防抖。当时刚开始写博客,还很青涩,各位要是不嫌弃,可以简单扫一眼,链接在下面。
今天我们一块研究的设计模式,就跟节流有关,这个东西平时不常用,但是一旦遇到相关问题后,不用还不行。大家也看到了,关于节流的文章,算上正在写的这篇,一共有4篇博客了,为啥我会对这个出现频率并不高的词这么关注?这说起来还有点历史。
想当年我还是个小萌新的时候,遇到一个需求,点击一个区域,调用一个接口,然后把查询的数据更新到页面上。这个普通不能再普通的需求,开启了我新世界的大门。
正常来讲上面的需求没什么特别的,但是嘞,转测试之后,测试同事在疯狂的点击那个请求区域,导致请求快速送,虽然是同一个请求,但是返回时间有快有慢,这就有可能导致,先发送的请求后返回过来,那数据显示就存在问题了。
对于当时的我,虽然有问题,但是还在可控范围之内,我给它加个蒙层不就行了,上一个接口不返回信息,就不能调用下一次。
这样改问题是没了,但是交互体验不太好,而且后面增加了一个双击功能,这个方案就彻底歇菜了。有没有什么方法既能满足测试,又能满足需求呢?研究了好久,终于让我找到了节流。
节流模式
对重复的业务逻辑进行节流控制,执行最后一次操作并取消其他操作,以提高性能。
平时业务中比较常见的节流情况,就是在 scroll 事件里面添加动画了,比如我们浏览一面时旁边会有一个回滚到顶部的按钮,由于 scroll 事件会触发多次,那添加的动画也就会被执行多次。为了解决这一个问题,我们先要实现一个节流器。
//节流器
var throttle = function() {
//获取第一个参数
var isClear = arguments[0], fn;
//如果第一个参数是 boolean 类型,那么第一个参数表示是否清除计时器
if(typeof isClear === 'boolean') {
//第二个参数则为函数
fn = arguments[1];
//函数的计时器句柄存在,清除计时器
fn.__throttleID && clearTimeout(fn.__throttleID);
//通过计时器延迟函数的执行
} else {
//第一个参数为函数
fn = isClear;
//第二个参数为函数执行时的参数
param = arguments[1];
//对执行时的参数适配默认值,这里用以前学习的 extend 方法
var p = extend({
context: null, //执行函数执行时的作用域
args: [], //执行函数执行时的相关参数
time: 300 //执行函数延迟执行的时间
}, param);
//清除执行函数计时器句柄
arguments.callee(true, fn);
//为函数绑定计时器句柄,延迟执行函数
fn.__throttleID = setTimeout(function() {
//执行函数
fn.apply(p.context, p.args)
}, p.time)
}
}
对于返回顶部按钮,我们可以进行如下优化:
//首先引入jquery.js 与 easing.js 方便返回顶部动画实现
//返回顶部按钮动画
function moveScroll() {
var top = $(document).scrollTop();
$('#back').animate({
top: top + 300}, 400, 'easeOutCubic')
}
//监听页面滚动事件
$(window).on('scroll', function() {
//节流执行返回顶部按钮动画
throttle(moveScroll);
})
图片延迟加载
节流模式还有一个常见的重要应用,就是图片的延迟加载,我们在网上浏览图片的时候,都是加载当前页的图片,当往下滚动的时候,再加载新的图片,还有就是当用户把滑动条直接拉到底部的时候,由于上面的图片会优先加载,造成底部图片加载推迟,这种体验也不是很好,此时我们就可以使用节流模式来处理这些逻辑,使可视范围内的图片优先加载。
/**
* 截留延迟加载图片类
* param id 延迟加载图片的容器 id
* 注:图片格式如下<img src="img/loading.gif" alt="" data-src="img/i.jpg">
*/
function LazyLoad(id) {
//获取需要延迟加载图片的容器
this.container = document.getElementById(id);
//缓存图片
this.imgs = this.getImgs();
//执行逻辑
this.init();
}
//节流延迟加载图片类原型方法
LazyLoad.prototype = {
//起始执行逻辑
init: function() {
},
//获取延迟加载图片
getImgs: function() {
},
//加载图片
update: function() {
},
//判断图片是否在可视范围内
shouldShow: function() {
},
//获取元素在页面中的纵坐标位置
pageY: function(element) {
},
//绑定事件(简化版)
on: function(element, type, fn) {
},
//为窗口绑定 resize 事件与 scroll 事件
bindEvent: function() {
}
}
对于节流延迟加载图片类的原型方法 init 应该做两件事,初始化图片加载(即执行 update 方法)和为窗口绑定事件。
//起始执行逻辑
init: function() {
//加载当前视图图片
this.update();
//绑定事件
this.bindEvent();
}
对于获取容器内图片的 getImages 方法需要注意,为了方便操作获取的图片元素集合(类数组),需要将其转化成数组,由于在 IE中对获取到的元素集合直接执行数组方法 slice 会报错,故通过遍历每一个元素,并将其加入新数组中返回,来显性创建数组。
//获取延迟加载图片
getImage: function() {
//新数组容器
var arr = [];
//获取图片
var imgs = this.container.getElementsByTagName('img');
//将获取的图片转化为数组(IE下通过 Array.prototype.slice会报错)
for(var i = 0, len = imgs.length; i < len; i++) {
arr.push(imgs[i])
}
return arr;
}
对于加载图片方法 update,需要遍历每一个图片元素,如果处在可视区域内则加载并将其在图片缓存中清除。
//加载图片
update: function() {
//如果图片都加载完成 返回
if(!this.imgs.length) {
return;
}
//获取图片长度
var i = this.imgs.length;
//遍历图片
for(--i; i >= 0; i--) {
//如果图片在可视范围内
if(this.shouldShow(i)) {
//加载图片
this.imgs[i].src = this.imgs[i].getAttribute('data-src');
//清除缓存中的此图片
this.imgs.splice(i, 1);
}
}
}
对于判断图片是否在可视范围内的方法,是判断图片的上下边位置是否符合下列条件(由于页面左边关系,对于 y 坐标,由上至下依次增大,对于 x 坐标,由左至右依次增大):图片底部高度大于可视视图顶部高度并且图片底部高度小于可视视图底部高度。
//判断图片是否在可视范围内
shouldShow: function(i) {
//获取当前图片
var img = this.imgs[i],
//可视范围内顶部高度(页面滚动条 top 值)
scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
//可视范围内底部高度
scrollBottom = scrollTop + document.documentElement.clientHeight,
//图片的顶部位置
imgTop = this.pageY(img),
//图片的底部位置
imgBottom = imgTop + img.offsetHeight;
//判断图片是否在可视范围内,图片底部高度大于可视视图顶部高度并且图片底部高度
//小于可视视图底部高度,或者图片顶部高度大于可视视图顶部高度并且图片顶部高度
//小于可视视图底部高度
if(imgBottom > scrollTop && imgBottom < scrollBottom || (imgTop > scrollTop && imgTop < scrollBottom)) return true;
//不满足上面条件则返回 false
return false;
}
对于获取图片元素纵坐标方法 pageY 这是通过元素一级一级遍历其父元素,并累加每一级元素 offsetTop 值获取的。
//获取元素页面中的纵坐标位置
pageY: function() {
//如果元素有父元素
if(element.offsetParent) {
//返回元素 + 父元素高度
return element.offsetTop + this.pageY(element.offsetParent);
} else {
//否则返回元素高度
return element.offsetTop;
}
}
为了简化代码,我们简单实现一下 on 绑定事件方法。
//绑定事件
on: function(element, type, fn) {
if(element.addEventListener) {
addEventListener(type, fn, false);
} else {
element.attachEvent('on', type, fn, false);
}
}
最后一个方法,绑定事件则是对页面的 scroll 与 resize 事件的监听,为检测每一次交互中事件的最后一次执行,故需要对事件的回调函数做节流处理。
//为窗口绑定 resize 事件与 scroll 事件
bindEvent: function() {
var that = this;
this.on(window, 'resize', function() {
//节流处理更新图片逻辑
throttle(that.update, {
context: that});
});
this.on(window, 'scroll', function() {
//节流处理更新图片逻辑
throttle(that.update, {
context: that});
})
}
大功告成,接下来我们只需要实例化类就可以了。
//延迟加载 container 容器内的图片
new LazyLoad('container');