scrollTop记录滚动位置

前文提到,需要记录滚动位置,

vue-router提供了一个方法 scrollBehavior,但是这个方法必须要在history模式下才能使用,具体参考文档

使用的方式如下

const scrollBehavior = (to, from, savedPosition) => {
   if (savedPosition) {
   setTimeout(() => {
     window.scrollTo(savedPosition.x, savedPosition.y)
   }, 200)
   }
}

export default new VueRouter({
    mode: 'history',
    scrollBehavior,
    routes: [{....}]
}]

但是我采用的是hash模式,这个方法便不能采用了,只能采用其他的方案。

笔者的思路是给滚动事件添加监听,在离开页面的时候纪录当时的scrollTop,在返回页面的时候设置scrollTop值为离开的时候的纪录值。

首先要拿到scrollTop的值,

由于存在兼容性问题,在拿到scrollTop的值的时候需要考虑各种情况。网上也有各种解决方案

各浏览器下 scrollTop的差异
IE6/7/8:
对于没有doctype声明的页面里可以使用 document.body.scrollTop 来获取 scrollTop高度 ;
对于有doctype声明的页面则可以使用 document.documentElement.scrollTop;
Safari:
safari 比较特别,有自己获取scrollTop的函数 : window.pageYOffset ;
Firefox:
火狐等等相对标准些的浏览器就省心多了,直接用 document.documentElement.scrollTop ;

获取scrollTop值
完美的获取scrollTop 赋值短语 :
var scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
通过这句赋值就能在任何情况下获得scrollTop 值。
仔细观察这句赋值,你发现啥了没??
没错, 就是 window.pageYOffset (Safari) 被放置在 || 的中间位置。
因为当数字0 与 undefine 进行 或运算时,系统默认返回最后一个值。即或运算中 0 == undefine ;
当页面滚动条刚好在最顶端,即scrollTop值为 0 时。 IE 下 window.pageYOffset (Safari) 返回为 undefine ,此时将window.pageYOffset (Safari) 放在或运算最后面时, scrollTop 返回 undefine , undefine 用在接下去的运算就会报错咯。
而其他浏览器 无论 scrollTop 赋值或运算顺序如何都不会返回 undefine. 可以安全使用.. 所以说到头还是IE的问题咯. 

documentElement 和 body 相关说明:
body是DOM对象里的body子节点,即 标签;
documentElement 是整个节点树的根节点root,即 标签;
DOM把层次中的每一个对象都称之为节点,就是一个层次结构,你可以理解为一个树形结构,就像我们的目录一样,一个根目录,根目录下有子目录,子目录下还有子目录。
以HTML超文本标记语言为例:整个文档的一个根就是,在DOM中可以使用document.documentElement来访问它,它就是整个节点树的根节点。而body是子节点,要访问到body标签,在脚本中应该写:document.body。

为scroll事件添加监听

为了监听到scroll事件并进行操作,需要给scroll事件添加监听函数。

addEventListener()与removeEventListener()用于处理指定和删除事件处理程序操作。

它们都接受3个参数:如 addEventListener("事件名" , "事件处理函数" , "布尔值"); (注:事件名不含"on",如“click”)

现在的版本可以省略第三个参数,默认值为false

需要注意的是,在addEventListener()的时候传入的函数不能是匿名函数,不然的话在remove的时候并不会去除监听。

笔者采用vue的自定义指令来完成。在定义指令的时候有两个钩子函数,bind和unbind,分别在这两个函数内部进行监听和移除监听。

bind: function (el, binding, vnode, oldVnode) {
  document.addEventListener('scroll', scrollHandler)
},
unbind: function (el, binding, vnode, oldVnode) {
  document.removeEventListener('scroll', scrollHandler)
}

之后,需要设置scrollTop的位置了, 

首先拿到存储的scrollTop值,笔者是存在sessionStorage里的。

var scrollTop = window.sessionStorage.getItem('topSave')

直接将拿到的值赋给document.documentElement.scrollTop时发现跟预想的结果不一样,打印出scrollTop的值发现,中间会有突变,猜测应该是浏览器内部也对scrollTop的值进行了设置,因此采用迂回的方式,设置延迟,将值赋给相应的scrollTop,具体代码如下

setTimeout(function () {
  if (scrollTop) {
     document.documentElement.scrollTop = scrollTop
     window.pageYOffset = scrollTop
     document.body.scrollTop = scrollTop
  }
}, 3500)

采用setTimeout的方式,防止设置的时候还没加载完,其中,scrollTop是存在sessionStorage里的,将这个函数放在bind里,保证只执行一次。

完成这个操作后,在chrome浏览器和安卓设备上都可以完成恢复到记录位置的功能。

但是setTimeout的问题是,1)卡顿,具体的表现是,回到列表页之后到恢复滚动位置有一定的时间间隔,在这个时间内用户可能已经有了其他的操作,突然回到一个位置导致卡顿;2)不起作用,具体的表现是,页面的数据需要网络获取,但是当数据返回时尚未渲染的时候已经完成了赋值操作,导致回到顶部无效。针对这两个问题可以采取的解决方案有: 将赋值的操作放到document.ready()里,参见文章。

测试发现问题

1)在iOS设备上却没有效果。始终回到页面顶部,即scrollTop始终为0       

2)  当滚动距离超过一定的大小之后,无论存储的scrollTop为多大的值,在设置完之后,scrollTop始终被设置为固定的一个值(笔者这边是591)

针对这两个问题的解释,参见文章,摘录如下

从详情页返回到列表页面, 列表会重新渲染, 时序大概是这样:
返回列表页 1
渲染页面 2

而浏览器恢复滚动条的位置的操作, 是在 1 和 2 之间, 这个时候就出问题了:
如果你页面上面的数据都是渲染出来的, 浏览器就会发现:

页面的高度<=屏幕的高度, 不存在滚动条, 此时 document.body.scrollTop = 0;
所以会设置 document.body.scrollTop = 0
修改了 document.body.scrollTop 触发了 scroll 事件, scroll 里面又重写了 pos
等你数据渲染结束之后, 读到的就是 0了.

如果发现你页面高度大于屏幕高度, 但是页面高度是 n, 而 pos 的值是: n + x,
比当前页面的最大的 scrollTop 值还大,
这个时候, document.body.scrollTop 的值就会等于 n.

解决方案参考

所以我们要解决这个问题.

当然是想到了 keep-alive, 刚启用的时候, 发现的确不错. 但是同时也发现:
列表项目靠前的, 往返操作的定位都很准, 越往后越不行, 直接拉到底, 再返回发现定位到的一般都是第二个第三个列表项目.
所以这个就很有意思了, 我大概猜测了一下浏览器的滚动位置恢复行为:
当 hashchange 的时候。拿到当前页面的 document.body.scrollTop 值, 和自己存储的滚动条位置。

二者取最小的值, 设置成当前的 document.body.scrollTop 的值, 当使用 keep-alive 的时候,

因为 hashchange 事件处理和页面渲染是并行的, 所以有时hashchange 拿到的 document 的高度是

已经渲染过几个元素的高度,

这个就是为什么定不准的原因.
好吧, 现在的情况是:
keep-alive 定不准, 不可靠, 所以需要我们自己来重新定位.
ok, 1 先绑定 scroll 事件:
var map = {};
window.onscroll = function() {
map[location.hash] = document.body.scrollTop;
}

2 再屏蔽掉浏览器自动恢复滚动位置行为带来的影响
a 在 hashchange 时强制 document.body.scrollTop = 0
b 在 scroll 事件里面, 当 document.body.scrollTop = 0 的时候不做 存操作.
var map = {};
window.onhashchange = function() {
document.body.scrollTop = 0;
}
window.onscroll = function() {
if (document.body.scrollTop) {
// 存
map[location.hash] = document.body.scrollTop;
} else {
// 读
}
}

3 在读操作里面, 设置一个定时任务, 去判断 document.body.scrollTop 的值和你保存的位置是不是相同的
var map = {};
window.onhashchange = function() {
document.body.scrollTop = 0;
}
window.onscroll = function() {
if (document.body.scrollTop) {
// 存
map[location.hash] = document.body.scrollTop;
} else {
var timer = null;
timer = setInterval(function(){
if (document.body.scrollTop == map[location.hash]) {
clearInterval(timer);
} else {
document.body.scrollTop = map[location.hash];
}
}, 20);
}
}

文章给了针对iOS多页面混用一个scroll事件处理的解决方案

具体的思路是在
1、在每个需要用vue-router切换的组件的mounted钩子内将页面的位置自动回滚到页面顶部,解决滚动条位置自动记录问题;
2、在每个组件内定义一条变量scrollWatch默认为true,在绑定滚动监听事件时加个if判断,只有在scrollWatch为true时进行

监听函数,然后在组件destroyed的钩子内将变量scrollWatch设为false;这样就解决了滚动监听在别的组件内仍会运行的问题。

参考代码如下:

2017.11.6

以上就是关于记录滚动位置的解决方案。一些只是思路,尝试没有起作用,需要后期再思考完善,之后再附。。


参考网址

[1] http://www.cnblogs.com/xwgli/p/3490466.html

[2] http://blog.csdn.net/qq_29606781/article/details/67650869

[3] https://cn.vuejs.org/v2/guide/custom-directive.html

[4] https://router.vuejs.org/zh-cn/advanced/scroll-behavior.html

[5] http://www.jb51.net/article/118592.htm

[6] http://wowtianwen.iteye.com/blog/2100913

[7] http://www.jb51.net/article/107864.htm

你可能感兴趣的:(笔记,Vue.js)