在网上浏览发现很多页面都提供了”返回顶部”功能,就是当你向下滚动页面时,会在页面右下方或者其他某个位置出现一个按钮,点击这个按钮,页面会自动回到顶部。
我很喜欢这个功能,但并非所有页面都提供了这样的按钮,所以我很自然的就想到用GreaseMonkey来实现,其实代码在去年的时候就已经写完, 但我当时对于JavaScript的了解实在有限,在重写脚本的过程中,不断发现过去从未关注过的问题,这样的过程对我来说很有意义,因为能够发现自己是真的进步了,下面就将整个过程分析一下,核心代码仍然十分简单。
这一段是去年写的,其中省略的那部分是一张图片的base64代码:

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照搬的:

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; 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-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;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