前期调研

当前浏览器的标准也越来越明确, 用户体验越来越重要. 相比于FireFox, Chrome没有自带平滑滚动, 鼠标滚轮滚动时直接滚动到指定位置, 没有实现缓动, 这应该是出于性能的考虑. 那么, 如何手工去实现平滑滚动, 或者说是带动画的滚动呢?
首先, Mark几个不错的开源项目, 打开传送门

  • No1. nicescroll : 平滑滚动的jQuery插件, 兼容性非常不错
  • No2. fullPage : 整页滚动jQuery插件, 适合极简风格的介绍型页面
  • No3. scrollTo : 平滑滚动的jQuery插件+1, 非常常用且轻量

其实, 手动实现一个这样的效果并不困难, 只要做以下两件事即可

  1. 截获mousewheel事件, 阻止浏览器默认行为
  2. 按照事件参数处理滚轮事件, 并通过动画的形式实现

踩坑和踩坑

浏览器兼容性

不同浏览器对滚轮事件的绑定都是不一样的, 比如

  • IE下是这样的:

    1
    2
    3
    if(document.attachEvent){
    document.attachEvent('onmousewheel',smoothBar);
    }
  • FF下是这样的(FF自带了平滑滚动, 实际情况下无需绑定FF的滚轮事件)

    1
    2
    3
    if(document.addEventListener){
      document.addEventListener('DOMMouseScroll',smoothBar,false);
    }
  • Chrome/Safari下是这样的

    1
    window.onmousewheel=document.onmousewheel=smoothBar;

还是浏览器的兼容性问题

稍低版本的IE浏览器事件没有target属性, 也没有preventDefault函数
这里是一个简单的fix方法

1
2
3
4
5
6
function(eventToFix{
if (eventToFix && eventToFix.target) return eventToFix;
eventToFix=eventToFix|| window.event;
eventToFix.preventDefault = function(this.returnValue = false; };
return eventToFix;
}

仍然是浏览器的兼容性

设置或获取当前滚动位置在不同浏览器也是不同的, 下面这句话能够兼容的获取滚动位置

1
2
3
var scrollTop = document.documentElement.scrollTop || 
window.pageYOffset ||
document.body.scrollTop;

其中, document.pageYOffset是Safari专用的.
其他的坑比如IE9以下还没有requestAnimationFrame函数等等, 浏览器兼容性是个超大的坑, 在IE下没能正常跑出来, 最后其实做的是一个Chrome下的原生JS平滑滚动, 不支持IE和FF.

如何判断元素是否可滚动

这是关键性的问题, 当截流了所有的mousewheel事件后, 一个页面可能有很多scrollbar, 如何根据截取的事件判断应该让哪个元素滚起来呢?
大概逻辑是这样的:

  • 如果event.target是body元素, 直接滚body
  • 如果event.target是其他元素, 判断这个元素能不能滚, 如果不能, 判断父节点能不能滚直到找到滚的起来的或body元素
    参考了一些资料, 发现有两种判断方式:
  1. 只要element.scrollHeight > element.clientHeight, 说明是个能滚的元素
  2. 见下图
    stackoverflow

这两种都是不准确的, scrollHeight > clientHeight不一定是有滚动条, 可能有其他原因, 具体原因尚待验证,
其次overflow:visible的元素也可能是有滚动条的

1
2
3
4
5
6
7
8
9
10
11
/* 原生JS代码 */
function isScrollable(element) {
var overflowY = window.getComputedStyle(element)['overflow-y']; //需要根据计算后的style判断而不能根据元素的css属性
return (overflowY === 'scroll' || overflowY === 'auto') && node.scrollHeight > node.clientHeight;
}
/* jQuery写法 */
$(element).height() > element.clientHeight &&
(
$(element).css('overflow-Y') === 'scroll' ||
$(element).css('overflow-Y') === 'auto'
);

实践与结果

实现思路已经很明确, 截取滚轮事件和判断元素是否可滚动已经理解, 只欠把对应的元素用动画滚起来了.
前端实现动画有几种方法:

  1. css3 transition动画或animation+keyframes实现, css并不支持scrollTop属性的动画, 支持动画的css属性有这些
  2. jQuery插件实现, 不用自己造轮子
  3. 使用requestAnimationFrame函数自己写一个高性能的平滑滚动, 享受造轮子的乐趣

代码如下~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
window.onmousewheel=document.onmousewheel=smoothBar;
function smoothBar(e){
//fix事件属性的差异. 曾经美好的浏览器兼容的愿望↓
var event = (function(eventToFix{
if (eventToFix && eventToFix.target) return eventToFix;
eventToFix=eventToFix|| window.event;
eventToFix.preventDefault =function(this.returnValue = false; };
return eventToFix;
})(e);
  event.preventDefault();

var counter = 0; //记录当前帧数
var maxCount = 90; //90个帧的动画
var progress = 0; //当前百分比
var scrollCache = {}; //元素是否可滚动缓存
var proCache = null;
if(window.WeakMap) {
//有weakmap使用它当缓存更合适
proCache = new WeakMap();
}
var delta = -event.wheelDelta; //滚动量
if(typeof requestAnimationFrame === "undefined") {
var requestAnimationFrame = setTimeout; //为了兼容~
}
function generateDelta() {
counter++;
/* 用sin函数做了个简单的ease,略带惯性效果 */
var tempProgress = Math.sin(counter/maxCount * Math.PI/2);
var tempResult = delta * (tempProgress - progress);
progress = tempProgress;
/* 计算出的当前帧应滚数值 */
return tempResult;
}

function srollAbleEle(node) {
var overflowY = window.getComputedStyle(node)['overflow-y'];
return (overflowY === 'scroll' || overflowY === 'auto') && node.scrollHeight > node.clientHeight;
}

/* 判断元素是否可滚动 */
function isScrollable(node) {
if(proCache) {
if(!proCache.has(node)) {
proCache.set(node, srollAbleEle(node));
}
return proCache.get(node);
} else {
if(typeof scrollCache[node.innerHTML] === 'undefined') {
scrollCache[node.innerHTML] = srollAbleEle(node);
}
return scrollCache[node.innerHTML]; //缓存后就不用每次计算了(在元素不会动态改变属性的前提下)
}
}
function getScrollElement(element){
if(isScrollable(element) || element.tagName === "BODY"){
return element;
}
//如果不能滚动, 找到第一个能滚的祖先
return getScrollElement(element.parentNode);
}

//如果页面只有一个可竖向滚动的元素, 直接指定比实时计算快很多
//var scrollEle = $("#scrollDiv")[0];
(function update() {
/* 在浏览器渲染的下一帧执行滚动 */
requestAnimationFrame(function(){
var deltaVal = generateDelta();
if(!!event.target) {
var scrollEle = getScrollElement(event.target);
scrollEle.scrollTop += deltaVal;
} else {
document.body.scrollTop += deltaVal;
}
if(counter === maxCount) return;
update();
});
})();
}