在网上浏览发现很多页面都提供了”返回顶部”功能,就是当你向下滚动页面时,会在页面右下方或者其他某个位置出现一个按钮,点击这个按钮,页面会自动回到顶部。
我很喜欢这个功能,但并非所有页面都提供了这样的按钮,所以我很自然的就想到用GreaseMonkey来实现,其实代码在去年的时候就已经写完, 但我当时对于JavaScript的了解实在有限,在重写脚本的过程中,不断发现过去从未关注过的问题,这样的过程对我来说很有意义,因为能够发现自己是真的进步了,下面就将整个过程分析一下,核心代码仍然十分简单。
这一段是去年写的,其中省略的那部分是一张图片的base64代码:
View Code
1 var imgDiv = document.getElementById('#toTheTop');
2
3 function createDiv() {
4
5 imgDiv = document.createElement('div');
6
7 imgDiv.id = '#toTheTop';
8
9 imgDiv.style.position = 'fixed';
10
11 imgDiv.style.display = 'none';
12
13 imgDiv.style.left = '90%';
14
15 imgDiv.style.top = '90%';
16
17 imgDiv.innerHTML = "<img title='Go To The Top!' style='z-index:999999; cursor:pointer; opacity:0.7;' src='data:image/png;base64,省略的图片代码' >";
18
19 document.body.appendChild(imgDiv);
20
21 imgDiv.addEventListener('click', function() {
22
23 window.scrollTo(0, 0);
24
25 } ,false);
26
27 }
28
29
30
31 window.addEventListener('scroll', function() {
32
33 if(pageHeight() - scrollY() - windowHeight() <= parseInt(pageHeight()) / 2) {
34
35 if(!imgDiv) {
36
37 createDiv();
38
39 }
40
41 imgDiv.style.display = 'block';
42
43 } else {
44
45 if(!imgDiv) {
46
47 createDiv();
48
49 }
50
51 imgDiv.style.display = 'none';
52
53 }
54
55 }, false);
56
57
58
59 function pageHeight() {
60
61 return document.body.scrollHeight;
62
63 }
64
65 function scrollY() {
66
67 //ie6 strict模式里的快捷方式
68
69 var de = document.documentElement;
70
71 //如果浏览器的pageYOffset可用,则使用之
72
73 return self.pageYOffset ||
74
75 //否则,尝试取得根节点的垂直滚动量
76
77 ( de && de.scrollTop ) ||
78
79 //最后,尝试取得body元素的垂直滚动量
80
81 document.body.scrollTop;
82
83 }
84
85 //取得视口高度
86
87 function windowHeight() {
88
89 //ie6 strict模式里的快捷方式
90
91 var de = document.documentElement;
92
93 //如果浏览器的innerHeight可用,则使用它
94
95 return self.innerHeight ||
96
97 //否则,尝试获得根节点的高度
98
99 ( de && de.clientHeight ) ||
100
101 //最后,尝试获得body元素的高度
102
103 document.body.clientHeight;
104
105 }
这段代码可以工作,但问题不少:
对div样式的设置十分麻烦,好的做法是将样式写进css中,这样阅读起来更紧凑,代码也会相对短一些。
window的scroll事件绑定的方法中,像判断imgDiv对象是否存在的代码被写了两次,而且很明显的,imgDiv是全局变量。
scrollY与windowHeight两个函数都是我从John Resig写的《精通JavaScript》中抄下来的,其实既然我已经使用了GreaseMonkey,那么就表明浏览器肯定是 Firefox(chrome也开始支持GreaseMonkey的脚本了),那么一些判断就没有必要,当然,这个问题可以忽略。
我在测试过程中还发现一个性能问题,就是scroll事件中第一行判断的代码,在每一次页面滚动时,这个表达式都会被计算一次,可实际上,pageHeight这个函数的返回值代表的是当前页面的真实高度,计算一次之后就不会变化,多次计算明显是一种性能上的浪费。
开始解决上面出现的问题吧,首先就是全局变量的问题,良好的编码习惯是尽量少出现全局变量,最简单的方式就是将代码放进一个匿名函数中,例如:
(function() {
//要执行的代码
}())
接下来是关于代码组织的问题,原始代码写了4个函数,都是在scroll事件绑定的函数中调用,大部分的逻辑判断也写在这里面,显得很凌乱。
首先来分析下,当scroll事件发生时,你想做什么?
我的想法是,当滚动条离开顶部向下滚动时,我需要显示一个按钮,当滚动条回到顶部时,我需要这个按钮隐藏。我可以定义一个scroll对象,在对象内定义show与hide两个函数,用来控制按钮的显示与隐藏,我还需要做一个判断,到底什么时候才让按钮显示?
看看原来代码是怎么写的,pageHeight() – scrollY() – windowHeight() <= parseInt(pageHeight()) / 2,我确实不知道当时怎么想的……为什么要这么写?实际上这里面只有scrollY()是必要的,就是滚动条与顶部的距离,当这个距离大于0时,表明开始滚动,这个时候就可以让按钮显示,如果这个值为0,按钮隐藏,就这么简单,根本不需要做那些运算。
好吧,我把scrollY()这个函数也加进scroll对象中,把它的名字改成getScrollY(),让它的意思更明显一些,这样scroll事件中的代码就可以写成:
1 if(scroll.getScrollY() > 0) {
2 scroll.show();
3 } else {
4 scroll.hide();
5 }
看起来还不错,但是稍微有些长,if else这样的判断可以用另外的运算符代替,那是什么呢?答案是三目运算符!
(scroll.getScrollY() > 0) ? scroll.show() : scroll.hide();
暂且不管这算不算是良好的编码习惯,但它确实很短,也比原来的代码cool一些:)
接下来再看滚动的代码,只有一行:window.scrollTo(0, 0);两个参数表示需要滚动的坐标,这段代码没有问题,但点完按钮后,页面立刻就回到了顶部,太直接了,如果能有一些动画效果是不是更好呢?
做法就是周期性的调用window.scrollTo方法,将纵坐标作为变量传进去,这样就可以模拟滚动的动画效果了。我们已经能够使用getScrollY()获得滚动条和顶部的距离,将这个距离作为变量传递进去,并不断将其减少,直到变成0,这就是接下来要做的事情:
我在scroll对象中定义了一个属性_scrollY用于保存距离值,
this._scrollY -= 100;
window.scrollTo(0, this._scrollY);
这就是主要代码,接下来的问题是如何周期性的调用它,在这里使用了setTimeout:
if(this._scrollY > 0) {
setTimeout(回调函数, 10);
}
当10毫秒之后就会调用回调函数,但是”回调函数”究竟是什么?
实际上我在scroll对象中定义了一个scrollToTop函数,上面关于滚动的代码就写在里面,也就是说这里的回调函数实际上就是scroll.scrollToTop,但这么写是有问题的,问题就出现在this上面。
如果直接把scroll.scrollToTop传进去,那么当运行时,scrollToTop中的this并非是scroll对象,而是全局对象,这样 this._scrollY的值就是undefined,但是,这并不影响滚动,因为我测试发现,最后的情况和直接调用 window.scrollTo(0, 0)的效果是一样。
解决这个问题的关键,就是要让scrollToTop中的this绑定到正确的对象即scroll上去,这里我使用了一个叫做bind的函数,是从上面提到的书中提供的:
function bind(context, name) {
return function() {
return context[name].apply(context, arguments);
}
}
apply方法可以将函数绑定到指定的对象上,所以最后传入的回调函数是:
bind(scroll, ‘scrollToTop’);
按理说代码写的差不多了,但我后来又发现,当调用window.scrollTo方法时,scroll事件也会被触发,也就是说绑定到事件中的代码还是会被执行,原来写的代码只会调用一次window.scrollTo,所以不用考虑这种情况,但现在会周期的调用这个方法,scroll事件也会周期的被触发,为了性能上的考虑,我在scroll对象中增加了一个布尔值,用于判断当前的滚动事件是由用户触发,还是由scrollToTop这个函数触发。
在脚本写完后不久,当我浏览一个博客时,我发现点击这个返回顶部按钮,页面竟然跳转到了博客的首页,后来检查发现是因为a标签的问题,原因大概是博客程序对a标签做了处理,总之解决办法是使用span来代替a,这样就解决了这个问题。
最后的代码在下面,这里提一下,按钮的样式是我从www.khanacademy.org照搬的:
View Code
1 (function(global) {
2
3 if(global !== window) return;
4
5
6
7 function bind(context, name) {
8
9 return function() {
10
11 return context[name].apply(context, arguments);
12
13 }
14
15 }
16
17
18
19 global.addEventListener('scroll', scrollHandler, false);
20
21
22
23 function scrollHandler() {
24
25 if(!scroll.isScrolling) {
26
27 (scroll.getScrollY() > 0) ? scroll.show() : scroll.hide();
28
29 }
30
31 }
32
33
34
35 var scroll = {
36
37 _scrollY : 0,
38
39 isScrolling : false, //is scrolling
40
41 imgBtn : null,
42
43 closeBtn : null,
44
45 create : function() {
46
47 var div = global.document.createElement('div');
48
49 var css = '#_scrollToTop{position:fixed;display:none;left:90%;top:80%;text-align:center;z-index:999999; width:50px;height:50px;cursor:pointer;opacity:0.5;} #_scrollToTop:hover{opacity:1;} #_scrollToTop a{text-decoration:none;} #_scrollToTop span._arrow{background:none repeat scroll 0 0 #eee;border-style:solid; border-width:1px;border-color:#ccc #ccc #aaa; border-radius:5px;color:#333;font-size:36px;padding:5px 10px;} #_scrollToTop span._close {background:repeat scroll #548b02;position:absolute;top:-15px;right:-15px;border-radius:15px;border:1px solid #ccc;width:15px;height:15px;font-size:12px;text-align:center;visibility:hidden;}';
50
51 GM_addStyle(css);
52
53
54
55 div.id = '_scrollToTop';
56
57 div.title = 'Back To Top';
58
59 div.innerHTML = '<span class="_close" title="hide this button">×</span><span class="_arrow">▲</span>';
60
61 document.body.appendChild(div);
62
63 div.addEventListener('click', bind(this, 'scrollToTop'),false);
64
65 div.addEventListener('mouseover', bind(this, 'mouseOver'),false);
66
67 div.addEventListener('mouseout', bind(this, 'mouseOut'),false);
68
69
70
71 return this.imgBtn = div;
72
73 },
74
75 getImgBtn : function() {
76
77 return this.imgBtn || this.create();
78
79 },
80
81 getCloseBtn : function() {
82
83 return this.closeBtn || (this.closeBtn = this.getImgBtn().getElementsByTagName('span')[0]);
84
85 },
86
87 show : function() {
88
89 this.getImgBtn().style.display = 'block';
90
91 },
92
93 hide : function() {
94
95 this.getImgBtn().style.display = 'none';
96
97 },
98
99 mouseOver : function() {
100
101 this.getCloseBtn().style.visibility = 'visible';
102
103 },
104
105 mouseOut : function() {
106
107 this.getCloseBtn().style.visibility = 'hidden';
108
109 },
110
111 getScrollY : function() {
112
113 //this piece of code is from John Resig's book 'Pro JavaScript Techniques'
114
115 var de = document.documentElement;
116
117 return this._scrollY = (self.pageYOffset ||
118
119 ( de && de.scrollTop ) ||
120
121 document.body.scrollTop);
122
123 },
124
125 scrollToTop : function(e) {
126
127 if(e && e.target && e.target.getAttribute('class') === '_close') {
128
129 //e.preventDefault();
130
131 this.hide();
132
133 global.removeEventListener('scroll', scrollHandler, false);
134
135 return false;
136
137 } else {
138
139 if(!this.isScrolling) {
140
141 this.isScrolling = true;
142
143 }
144
145 this._scrollY -= 150;
146
147 global.scrollTo(0, this._scrollY);
148
149 if(this._scrollY > 0) {
150
151 setTimeout(bind(scroll, 'scrollToTop'), 20);
152
153 } else {
154
155 this.isScrolling = false;
156
157 }
158
159 }
160
161 }
162
163 }
164
165 }(window.top))
这里面多了一个隐藏按钮,点击X标志,可以让隐藏返回顶部这个功能,实际使用中发现,这个隐藏按钮的位置在许多网页都不相同,暂时没有去解决这个问题。
我还对iframe做了一些检测,比如说我使用Gmail写邮件,或者用wordpress后台写博客,这个按钮总会出现在正文中,这是没有必要的,所以我判断只有当window和top值相等时,才让这个脚本继续运行,否则就返回。这样的结果就是在Gmail中按钮不会显示了:)
另外,setTimeout的时间间隔为10毫秒,这个总感觉不是很对,因为对定时器还没有怎么仔细学习过,等把John Resig那本新书读完再说吧。
最后附上这个脚本在UserScript网站的地址:http://userscripts.org/scripts/show/115493

浙公网安备 33010602011771号