最初的api设计
<lf-sticky distance="100">
<div>我是粘住的内容</div>
</lf-sticky>
上面的api就是接受一个distance参数,意思是距离顶部多少距离的时候固定住
当需要通过滚动事件获取当前元素距离文档顶部的距离时,不能只使用getBoundClinetRect这个api,因为他获取的是距离窗口顶部的位置,也就是下面的109,如果要获取当前元素剧文档的距离就需要使用getBoundClientRect.top+window.scrollY

- 简单实现
sticky.vue
<template>
<div class="lifa-sticky" ref="sticky" :class="classes">
<slot></slot>
</div>
</template>
<script>
export default {
name: "LiFaSticky",
data() {
return {
sticky: false
}
},
created() {
},
mounted() {
window.addEventListener('scroll', () => {
if(window.scrollY > this.top()) {
this.sticky = true
}else {
this.sticky = false
}
})
},
methods: {
top() {
let {top} = this.$refs.sticky.getBoundingClientRect()
top = top + window.scrollY
return top
}
},
computed: {
classes() {
return {
sticky: this.sticky
}
}
}
}
</script>
<style scoped lang="scss">
.lifa-sticky{
&.sticky {
position: fixed;
top: 0;
left: 0;
width: 100%;
}
}
</style>
问题1:当元素固定的时候会一直闪?
原因:因为我们fixed定位后,元素距离文档顶部的top就无法确定了,所以我们只能取一次,也就是把最初的top确定下来,而不是每次滚动的时候都去重新获取top
mounted() {
let top = this.top()
window.addEventListener('scroll', () => {
if(window.scrollY > top) {
this.sticky = true
}else {
this.sticky = false
}
})
},
问题2:当文档内容较少刚好达到显示滚动条的程度,你滚动滚动条会发现无法滚动,然后页面一直再闪
原因:

以上面图为例,比如说我们窗口最多显示9行内容,默认一开始顶部的元素还没sticky状态在文档流中,所以9就超出了窗口就会显示滚动条,而当我们滚动的时候,顶部的元素就直接会变成fixed定位脱离文档流,这时候页面中只剩1-9,正好是窗口显示的高度,所以滚动条消失,就不能滚动,滚动高度就是0,因此顶部的元素又进入了文档流,9又出去了又出现滚动条了,一直如此循环
解决办法:不要让当前元素脱离文档流,而是让他的子元素脱离,因为子元素脱离文档流了,所以会造成父元素没有高度,这时我们需要在一开始的时候获取到它的高度然后赋值给它
<template>
<div class="lifa-sticky-wrapper" ref="sticky">
<div class="lifa-sticky" :class="classes">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "LiFaSticky",
data() {
return {
sticky: false
}
},
created() {
},
mounted() {
// 在一开始还没有脱离文档流的时候获取一下父元素的高度,然后将他的
// 高度赋值给它,这样当脱离文档流的时候他也会有高度
let {top,height} = this.topAndHeight()
this.$refs.sticky.style.height = `${height}px`
window.addEventListener('scroll', () => {
if(window.scrollY > top) {
this.sticky = true
}else {
this.sticky = false
}
})
},
methods: {
topAndHeight() {
let {top,height} = this.$refs.sticky.getBoundingClientRect()
top = top + window.scrollY
return {top,height}
}
},
}
</script>
问题3:由于我们是在一开始就获取高度,如果这个子元素里有服务端获取的图片什么的,有可能因为网络原因我们不能获取到准确的高度
解决方法:在滚动的时候去获取它的高度
<lf-sticky distance="100">
<div style="border: 1px solid red;">
<img src="https://i.loli.net/2019/05/20/5ce2b21628eab50666.jpg" alt="">
</div>
</lf-sticky>
mounted(){
window.addEventListener('scroll', () => {
if(window.scrollY > top) {
// 滚动的时候直接获取高度,防止因为网络延迟原因获取高度不准确
let height = this.height()
this.$refs.sticky.style.height =`${height}px`
this.sticky = true
}else {
this.sticky = false
}
})
},
methods: {
height() {
let {height} = this.$refs.sticky.getBoundingClientRect()
return height
}
}

问题4:如果像上图中我们的内容宽度不是百分之百的,那么我们就不能直接在css里设置width和left,所以我们也要通过js获取
<template>
<div class="lifa-sticky-wrapper" ref="sticky" :style="{height}">
<div class="lifa-sticky" :class="classes" :style="{left,width}">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "LiFaSticky",
data() {
return {
sticky: false,
height: undefined,
width: undefined,
left: undefined
}
},
created() {
},
mounted() {
// 在一开始还没有脱离文档流的时候获取一下父元素的高度,然后将他的
// 高度赋值给它,这样当脱离文档流的时候他也会有高度
let top = this.top()
window.addEventListener('scroll', () => {
if(window.scrollY > top) {
// 滚动的时候直接获取高度,防止因为网络延迟原因获取高度不准确
let {left,width,height} = this.$refs.sticky.getBoundingClientRect()
this.left = left + 'px'
this.height = height + 'px'
this.width = width + 'px'
this.sticky = true
}else {
this.sticky = false
}
})
},
methods: {
top() {
let {top} = this.$refs.sticky.getBoundingClientRect()
top = top + window.scrollY
return top
}
},
computed: {
classes() {
return {
sticky: this.sticky
}
}
}
}
</script>
<style scoped lang="scss">
.lifa-sticky{
&.sticky {
position: fixed;
top: 0;
}
}
</style>
热更新与beforeDestroy

当我们修改代码的时候如果不自己刷新页面就会发现控制台会报错,原因是webpack的热更新机制,当我们修改代码的时候它会帮我们刷新对应的组件,也就是把之前的组件从页面移除,把新的组件加到页面中的相同位置,而我们旧的组件在监听当前窗口的scroll事件,新的也在监听,而旧的组件的dom已经从页面移除了,所以获取不到对应的值
技巧:如果你的组件做了任何有副作用的操作(你改了除了自己以外的任何东西,常见的就是window里的事件监听还有setInterval),这里因为你修改了window,你就必须在beforeDestory中把这个副作用清除掉
mounted() {
// 在一开始还没有脱离文档流的时候获取一下父元素的高度,然后将他的
// 高度赋值给它,这样当脱离文档流的时候他也会有高度
let top = this.top()
this.windowScrollHandler = () => {
if(window.scrollY > top) {
// 滚动的时候直接获取高度,防止因为网络延迟原因获取高度不准确
let {left,width,height} = this.$refs.sticky.getBoundingClientRect()
this.left = left + 'px'
this.height = height + 'px'
this.width = width + 'px'
this.sticky = true
}else {
this.sticky = false
}
}
window.addEventListener('scroll', this.windowScrollHandler)
},
methods: {
top() {
let {top} = this.$refs.sticky.getBoundingClientRect()
top = top + window.scrollY
return top
}
},
computed: {
classes() {
return {
sticky: this.sticky
}
}
},
beforeDestroy() {
window.removeEventListener('scroll',this.windowScrollHandler)
}
添加distance
<lf-sticky :distance="100">
<div style="border: 1px solid red;">
<img src="https://i.loli.net/2019/05/20/5ce2b50b09e2843554.png" alt="">
</div>
</lf-sticky>
- sticky.vue
<div class="lifa-sticky-wrapper" ref="sticky" :style="{height}">
<div class="lifa-sticky" :class="classes" :style="{left,width,top}">
<slot></slot>
</div>
</div>
props: {
distance: {
type: Number,
default: 0
}
},
data() {
return {
+ top: undefined
}
},
mounted() {
// 在一开始还没有脱离文档流的时候获取一下父元素的高度,然后将他的
// 高度赋值给它,这样当脱离文档流的时候他也会有高度
let top = this.offsetTop()
this.windowScrollHandler = () => {
if(window.scrollY > top - this.distance) {
// 滚动的时候直接获取高度,防止因为网络延迟原因获取高度不准确
let {left,width,height} = this.$refs.sticky.getBoundingClientRect()
this.left = left + 'px'
this.height = height + 'px'
this.width = width + 'px'
this.sticky = true
this.top = this.distance + 'px'
}else {
this.sticky = false
}
}
window.addEventListener('scroll', this.windowScrollHandler)
},
methods: {
offsetTop() {
let {top} = this.$refs.sticky.getBoundingClientRect()
top = top + window.scrollY
return top
}
},
网友评论