问题
这个公告栏在我的电脑上会错位,在手机上就不会
分析
根据经验,这个东西有两种可能,一是小数的舍入出现问题,而是高分辨率屏幕的问题。
调试
按 F12
打开开发者工具。定位到这个元素,看看 CSS 出现了什么问题。
比较奇怪的是,我把这个元素周围都找遍了,就是没有找到 CSS 的改变。
于是,我只能从代码开始分析了。
这个部分会在鼠标放在上面的时候停止滚动,所以我找一下 mouseover
的 EventListener
。
然后用左下角的 {}
按钮格式化代码。
Chrome 的开发者工具很人性化嘛。这个开发者工具可是相当牛逼了,真要是有教程的话,这都能写一本书。
格式化之后,就会定位到 EventListener
的位置。
我大致看了一下,这个代码使用的是 overflow: hidden
和 scrollTop
结合做出的滚动效果。并没有用到 CSS,并不是用 margin-top
、position: absolute
、top
或者 transform: translate(0,y)
的方法做的。
我们把代码复制出来,看看出了什么问题。
function announcement() {
var ann = new Object();
ann.anndelay = 3000;
ann.annst = 0;
ann.annstop = 0;
ann.annrowcount = 0;
ann.anncount = 0;
ann.annlis = $('anc').getElementsByTagName("li");
ann.annrows = new Array();
ann.announcementScroll = function() {
if (this.annstop) {
this.annst = setTimeout(function() {
ann.announcementScroll();
}, this.anndelay);
return;
}
if (!this.annst) {
// 以下部分为初始化代码
var lasttop = -1;
for (i = 0; i < this.annlis.length; i++) {
if (lasttop != this.annlis[i].offsetTop) {
if (lasttop == -1)
lasttop = 0;
// 计算两个元素的 offsetTop 的差值
this.annrows[this.annrowcount] = this.annlis[i].offsetTop - lasttop;
this.annrowcount++;
}
lasttop = this.annlis[i].offsetTop;
// 上面这一部分就是计算两个元素的 offsetTop 的差值
}
if (this.annrows.length == 1) {
$('an').onmouseover = $('an').onmouseout = null;
} else {
this.annrows[this.annrowcount] = this.annrows[1];
$('ancl').innerHTML += $('ancl').innerHTML;
this.annst = setTimeout(function() {
ann.announcementScroll();
}, this.anndelay);
$('an').onmouseover = function() {
// 鼠标悬浮时暂停
ann.annstop = 1;
}
;
$('an').onmouseout = function() {
ann.annstop = 0;
}
;
}
this.annrowcount = 1;
return;
}
if (this.annrowcount >= this.annrows.length) {
$('anc').scrollTop = 0;
this.annrowcount = 1;
this.annst = setTimeout(function() {
ann.announcementScroll();
}, this.anndelay);
} else {
this.anncount = 0;
// 滚动 this.annrows[this.annrowcount] 像素
this.announcementScrollnext(this.annrows[this.annrowcount]);
}
}
;
ann.announcementScrollnext = function(time) {
// 向下滚动 1 像素
$('anc').scrollTop++;
// 计数器 +1
this.anncount++;
if (this.anncount != time) {
this.annst = setTimeout(function() {
// 继续滚动
ann.announcementScrollnext(time);
}, 10);
} else {
// 滚动条目 +1
this.annrowcount++;
this.annst = setTimeout(function() {
// 准备下一次滚动
ann.announcementScroll();
}, this.anndelay);
}
}
;
ann.announcementScroll();
}
分析了一下代码,并不存在什么问题。
作为一个程序员,看到这句代码是非常反感的。
$('anc').scrollTop++;
this.anncount++;
if (this.anncount != time)
这种动画过程的东西,最好不要用“累加小量 + 次数判断”的方法,因为如果是浮点数,这里累加就容易出现偏差。
最好采用“系统时间均分总时间”的方法,这样可以保证动画过程不会因为卡顿而影响正常的时间进度(具体请自己体会,我就不细说了)。这种方法可能计算量比较大,涉及了取系统时间、乘除法(其他方法只有一次加法)。
当然一种折中的方案也可以,就是“累加小量 + 结果判断”。
敏锐的直觉告诉我,跟我的屏幕是高分辨率屏幕有关。
计算机屏幕通常是 72 dpi (dots per inch),我的高分屏是 96 dpi,这样一个像素看起来就会更小,于是 Windows 为了改善字小的问题,就有缩放的问题了。
右键点击桌面 > 显示
这是 Windows 的问题,Windows 想了缩放这么个想法,新的程序都要支持 DPI Aware
这个功能,否则就会被缩放。
Chrome 对这个东西的支持可以说非常好,而且还有很多特殊的属性可以使用,但是同时也代{过}{滤}理了某些问题。
我们在这条语句前后下个断点,单步执行,分析一下。
断点之后可以在下方的 Console 中执行语句,上下文就是当前断点处的上下文
为什么 $('anc').scrollTop++
执行之后 $('anc').scrollTop
的值只增长了 0.8
,0.8
的 1.25
倍正好就是 1
,看来的确和 DPI 有关。
我们来 Bing 一下 Chrome scrollTop DPI
。经过一段时间浏览我发现了这篇文章 https://bugs.chromium.org/p/chromium/issues/detail?id=224444
大概的意思就是说,Chrome 的滚动时会按滚动条正好滚动真实的 1px
为基本单位来滚动,但是 Chrome 的内容是按照 DPI 缩放过的,所以就会内容只滚动了 0.8px
。
也就是虽然执行的是 $('anc').scrollTop = $('anc').scrollTop + 1
,+1 也的确是 +1 了,但是加完之后会在向 0.8
来进行舍入,所以每次只加了 0.8
。
设想一下,如果我的屏幕缩放达到了 150%
,基本单位变成了 0.67
,而 1
正好处于 0.67
与 1.33
的中间,如果缩放大于 150% 的话,每次就会移动 2 基本单位的长度。
解决方案
问题分析完了,说一下解决方案吧。
其实这就是我反感原来那种写法的原因,会带来某些未知的问题。
解决方案也就是,把判断次数改成判断结果。
if (this.annrowcount >= this.annrows.length) {
$('anc').scrollTop = 0;
this.annrowcount = 1;
this.annst = setTimeout(function() {
ann.announcementScroll();
}, this.anndelay);
} else {
// 这里我只是借用了原来的变量,正常应该把这个变量名改成 annStartTop 之类的
this.anncount = $('anc').scrollTop;
// 滚动 this.annrows[this.annrowcount] 像素
this.announcementScrollnext(this.annrows[this.annrowcount]);
}
}
;
ann.announcementScrollnext = function(time) {
// 向下滚动 1 像素
$('anc').scrollTop++;
// 判断滚动差值
if (($('anc').scrollTop - this.anncount) < time) {
this.annst = setTimeout(function() {
ann.announcementScrollnext(time);
}, 10);
} else {
this.annrowcount++;
this.annst = setTimeout(function() {
ann.announcementScroll();
}, this.anndelay);
}
}
直接在断点的时候执行上述语句,把函数替换掉就可以测试了。
测试成功!
继续改进
上面这种方案依旧不好
// 计算两个元素的 offsetTop 的差值
this.annrows[this.annrowcount] = this.annlis[i].offsetTop - lasttop;
一开始使用这句代码,就将导致一系列问题。
因为 offsetTop 是正常的数值(通常是整数),而 scrollTop 是以 0.8px 为单位的数值,所以可能会有累积误差,大约是每滚动 5 条差 1px,虽然比之前的小多了,但是问题还是存在的。
我们必须从根本上解决问题
function announcement() {
var ann = new Object();
ann.anndelay = 3000;
ann.annst = 0;
ann.annstop = 0;
ann.annrowcount = 0;
ann.annlis = $('anc').getElementsByTagName("li");
ann.annrows = new Array();
ann.announcementScroll = function() {
if (this.annstop) {
this.annst = setTimeout(function() {
ann.announcementScroll();
}, this.anndelay);
return;
}
if (!this.annst) {
// 以下部分为初始化代码
var lasttop = -1;
for (i = 0; i < this.annlis.length; i++) {
if (lasttop != this.annlis[i].offsetTop) {
// 这里不再用相邻两个元素的差值了,而使用和第一个元素的差值
this.annrows[this.annrowcount] = this.annlis[i].offsetTop - this.annlis[0].offsetTop;
this.annrowcount++;
}
lasttop = this.annlis[i].offsetTop;
// 上面这一部分就是计算两个元素的 offsetTop 的差值
}
// 这里我也很反感,能用 <= 尽量不要用 ==,万一少写了一个等于号呢?
// 万一一条公告都没有呢?
if (this.annrows.length == 1) {
$('an').onmouseover = $('an').onmouseout = null;
} else {
this.annrows[this.annrowcount] = this.annrows[1];
$('ancl').innerHTML += $('ancl').innerHTML;
this.annst = setTimeout(function() {
ann.announcementScroll();
}, this.anndelay);
$('an').onmouseover = function() {
ann.annstop = 1;
}
;
$('an').onmouseout = function() {
ann.annstop = 0;
}
;
}
this.annrowcount = 1;
return;
}
if (this.annrowcount >= this.annrows.length) {
$('anc').scrollTop = 0;
this.annrowcount = 1;
this.annst = setTimeout(function() {
ann.announcementScroll();
}, this.anndelay);
} else {
// 滚动到 this.annrows[this.annrowcount] 位置
this.announcementScrollnext(this.annrows[this.annrowcount]);
}
}
;
ann.announcementScrollnext = function(targetTop) {
// 向下滚动 1 像素
$('anc').scrollTop++;
// 直接比较 scrollTop 和 targetTop
if ($('anc').scrollTop < targetTop) {
this.annst = setTimeout(function() {
ann.announcementScrollnext(targetTop);
}, 10);
} else {
this.annrowcount++;
this.annst = setTimeout(function() {
ann.announcementScroll();
}, this.anndelay);
}
}
;
ann.announcementScroll();
}
同样我们替换一下这个函数测试一下。
继续改进算法
此时,我又发现一个问题,如果屏幕缩放小于 100%
(屏幕缩放不能小于 100%,但是 Ctrl + 鼠标滚轮的方式可以将 Chrome 内容缩放调整到小于 100%),Chrome 对这个单位采取的舍入是向下取整,那么这个方法就会出现问题,$('anc').scrollTop++
这句话不会对 scrollTop
造成任何变化,因为滚动单位已经大于 1px 了。
原本的方法是计数,第一次修改是改成计算差值,第二次修改是直接验证我们想要的结果。这次我们就要改成根据初始位置和时间算中间位置了。
function announcement() {
var ann = new Object();
ann.anndelay = 3000;
ann.annst = 0;
ann.annstop = 0;
ann.annrowcount = 0;
ann.anncount = 0;
ann.annScrollTopBegin = 0;
ann.annlis = $('anc').getElementsByTagName("li");
ann.annrows = new Array();
ann.announcementScroll = function() {
if (this.annstop) {
this.annst = setTimeout(function() {
ann.announcementScroll();
}, this.anndelay);
return;
}
if (!this.annst) {
// 以下部分为初始化代码
var lasttop = -1;
for (i = 0; i < this.annlis.length; i++) {
if (lasttop != this.annlis[i].offsetTop) {
// 这里不再用相邻两个元素的差值了,而使用和第一个元素的差值
this.annrows[this.annrowcount] = this.annlis[i].offsetTop - this.annlis[0].offsetTop;
this.annrowcount++;
}
lasttop = this.annlis[i].offsetTop;
// 上面这一部分就是计算两个元素的 offsetTop 的差值
}
// 这里我也很反感,能用 <= 尽量不要用 ==,万一少写了一个等于号呢?
// 万一一条公告都没有呢?
if (this.annrows.length == 1) {
$('an').onmouseover = $('an').onmouseout = null;
} else {
this.annrows[this.annrowcount] = this.annrows[1];
$('ancl').innerHTML += $('ancl').innerHTML;
this.annst = setTimeout(function() {
ann.announcementScroll();
}, this.anndelay);
$('an').onmouseover = function() {
ann.annstop = 1;
}
;
$('an').onmouseout = function() {
ann.annstop = 0;
}
;
}
this.annrowcount = 1;
return;
}
if (this.annrowcount >= this.annrows.length) {
$('anc').scrollTop = 0;
this.annrowcount = 1;
this.annst = setTimeout(function() {
ann.announcementScroll();
}, this.anndelay);
} else {
this.anncount = 0;
this.annScrollTopBegin = $('anc').scrollTop;
// 滚动到 this.annrows[this.annrowcount] 位置
this.announcementScrollnext(this.annrows[this.annrowcount]);
}
}
;
ann.announcementScrollnext = function(targetTop) {
// 直接滚动到目标位置
$('anc').scrollTop = this.annScrollTopBegin + this.anncount;
// 计数器 +1
this.anncount++;
// 直接比较 scrollTop 和 targetTop
if ($('anc').scrollTop < targetTop) {
this.annst = setTimeout(function() {
ann.announcementScrollnext(targetTop);
}, 10);
} else {
this.annrowcount++;
this.annst = setTimeout(function() {
ann.announcementScroll();
}, this.anndelay);
}
}
;
ann.announcementScroll();
}
这个问题也解决了
其他
setTimeout、setInterval 的问题
还有另外一个需要注意的问题,Chrome 会在浏览器切换到后台之后停掉所有间隔小于 1s 的延时/循环。
参考:https://stackoverflow.com/questions/6032429/chrome-timeouts-interval-suspended-in-background-tabs
不过本例中并没有什么影响。
显示全部公告