#页面滚动刷新的实现原理 #下拉刷新#上拉刷新#drag to fresh
1.准备工作
准备一个最简单的页面如上图左,其页面结构仅两层,如右所示。
2. 分析
本demo的目的在于探究在开发任务中,页面拖动而触发某些特定行为的简单实现。
2.1 页面元素
上拉加载,下拉刷新也好,都是在页面拖动行为到一定时机,触发了某个事件的执行。
所以,只关心上拉和下拉这两个临界关系就好了。
对于上拉,的触发实际上是:
如果随便在页面上取一个参考点,例如最左上角,从初始状态一直上滑页面,该点所能够变化的范围,在松开屏幕后,所能保持的最大行径,即“页面触底时页面的超出范围”。他是一个最大静态值。
maxStaticHeight = 页面实体高度(scrollHeight )- 设备显示高度 (window.innerHeight)
maxStaticHeight 是我自己随便取的名字
2.2 触发时机
在移动端,页面实体部分,可以在触底之后继续上拉,松开后回弹到触底。也就是,依旧在页面最左上角取一个参考点,该点的实际能达到的最大行径其实是可以超出上面提到的 maxStaticHeight 。如下图:
如果将页面实际能够滚动的高度叫做dynamicHeight(动态高度),那么动态高度实际上是可以比maxStaticHeight 大的。
所以这就是关键, 我们拿这个动态的实际高度去和最大的静态高度(页面触底时页面的超出范围maxStaticHeight)作比较;
触发高度 = dynamicHeight - maxStaticHeight
3. 代码实现示例
首先,明确上面几个讨论参数的取值:
- scrollHeight :
页面元素.scrollHeight
页面实体的高度 - window.innerHeight :
window.innerHeight
设备显示页面的范围 - dynamicHeight :
windows.scrollY
实际滚动高度
页面scroll
事件
需要通过监听window.scroll
事件来处理在页面滚动过程中根据一些值变化关系来进行我们需要的逻辑处理。
<script>
document.addEventListener('scroll', scrollEventHandler)
function scrollEventHandler() {
console.log(window.scrollY)
}
</script>
像这样,就可以获取到实际滚动值了。 然后拿静态值maxStaticHeight
作差,就能得到我们需要去触发的值了。
<script>
window.onload = function() {
let window_innerHeight = window.innerHeight;
let scrollHeight = document.querySelector('body').scrollHeight;
let maxStaticHeight = scrollHeight - window_innerHeight;
let triggerGap
document.addEventListener('scroll', scrollEventHandler);
function scrollEventHandler() {
triggerGap = window.scrollY - maxStaticHeight;
console.log(triggerGap)
if (triggerGap >= 100) {
alert("trigger!!!")
}
}
}
</script>
此时如果需要下拉刷新就更加简单了:
仅判断window.scriollY
的反向滑动距离就可以了:
<script>
window.onload = function() {
let window_innerHeight = window.innerHeight;
let scrollHeight = document.querySelector('body').scrollHeight;
let maxStaticHeight = scrollHeight - window_innerHeight;
let triggerGap
let msg = document.querySelector('#msg')
document.addEventListener('scroll', scrollEventHandler);
function scrollEventHandler() {
msg.innerHTML = window.scrollY + "/" + "sh:" + scrollHeight + "/" + "wi:" + window_innerHeight + "/" + "msh:" + maxStaticHeight
triggerGap = window.scrollY - maxStaticHeight;
console.log(triggerGap)
if (triggerGap >= 100) {
alert("trigger drag up!!!")
}
if (window.scrollY <= -100) {
alert("trigger drag down!!!")
}
}
</script>
4. 测试
5.存在的问题,和改进
在以上的demo中,有一个很明显的问题,就是,监听了scroll
事件,而我们定义的行为触发条件是:
if (triggerGap >= 100) {
alert("trigger drag up!!!")
}
if (window.scrollY <= -100) {
alert("trigger drag down!!!")
}
这里用的是alert
所以条件满足时,立即被触发, 但是,如果是console,那就会被多次触发。
稍作变化,为了方便手机调试,把结果写在innerHTML
:
<!-- html -->
<div style="position: fixed;bottom:0;color:rgb(255, 0, 119)" id="count"></div>
//js 选中元素,并声明一个变量
let count = document.querySelector('#count')
let num = 0;
//条件稍作改变
if (triggerGap >= 100) {
console.log(num++)
count.innerHTML = "trigger Counts: " + num;
}
测试:
trigger Counts:48
可以看到,一次上拉操作,就能触发几十次了。 这显然不是我们希望的。
所以,这里就存在一个防抖/节流问题了, 具体的相关内容可以在这里参考。
简单的说,这个问题可以使用防抖的方式解决,你前面上拉多少次都没有用,只有在你松手后,也就是最后一次触发,才执行。 也可以使用节流的方式去解决,也就是你可以不停的上拉,但是我们让它不能无限的执行我们预定义的行为,而是有固定间隔的执行。
二者,如果以定义的行为是发送一次http请求的话,防抖就是在指定时间内,上拉,只会执行松手后的那一次请求发送。 而如果是节流的方式,那就是如果不停的上拉以触发,那么就以指定间隔去发送请求,例如一秒发送一次。
这里就直接使用节流的方式来解决了。当然还可以设计touchEnd()
事件来监听手指抬起后触发机制等方式来达到同样的目的。
let trigger = true;
function scrollEventHandler() {
msg.innerHTML = window.scrollY + "/" + "sh:" + scrollHeight + "/" + "wi:" + window_innerHeight + "/" + "msh:" + maxStaticHeight
triggerGap = window.scrollY - maxStaticHeight;
if (triggerGap > 100 && trigger) {
trigger = false;
setTimeout(() => {
count.innerHTML = "trigger Counts: " + num++;
trigger = true;
}, 1000)
}
}
6.补充更新 Element.getBoundingClientRect()
6.1 ViewPort概念
什么是ViewPort(视图), 在web中,通常指的是窗口中用于显示网页的区域
https://developer.mozilla.org/en-US/docs/Web/CSS/Viewport_concepts
任何一个元素都可以以ViewPort为参考坐标系确定其所在位置。
6.2 Element.getBoundingClientRect()
每个html元素对象,都提供了getBoundingClientRect()
方法, 调用这个方法,将会返回该元素相对ViewPort的一些位置信息。
在之前的探究中,知道,监听页面滑动,实际,就是监听元素在页面滚动时,相对ViewPort的垂直方向上的位置变化,之前是去计算元素的scrollHeight
(即整个元素的实体高度)和ViewPort的差值,即maxStaticHeight
,现在通过getBoundingClientRect()
我们可以更加简化这个过程。
例如,我们需要监听页面中scroll元素底部距离页面底部,也就是ViewPort底部的距离。 就直接可以得到:
bottomGap = window.innerHeight - Element.getBoundingClientRect().bottom
即:
对上面的例子再次改进后的在线demo和源码
一点说明
在实际的开发工作中,页面层级往往不是这么直接,所以可能会根据实际进行一些简单调整。 例如:
其结构大致类似这样:
当活动窗体在上下滑动到临界状态,例如:
可以看到,此时的我们监听的高度:
bottomGap = window.innerHeight - Element.getBoundingClientRect().bottom
实际上,是包含了“其他元素的高度的”,会影响我们对触发值的判断,所以在计算时,应该让下方的“其他元素”的高度参与计算,或者说,此时,不再应该是相对ViewPort计算,而是相对下方“其他元素”计算。不过实际上,就是减掉“下方其他元素”的高度即可。