首先是一些說明
- 動畫效果是原生 js 實現(xiàn),不可能為了這么一個動畫引用 Jquery 這么大一個庫
- 因為是在 vue-router 的 hash 模式下,所以就不能用
herf="#contentx"這樣的方式來跳轉(zhuǎn)了,這里用 js 來滾動到指定元素位置
文章整體思路是先布局,然后把內(nèi)容和導(dǎo)航聯(lián)動起來,最后來實現(xiàn)點擊導(dǎo)航滾動到指定內(nèi)容
布局
話不多說,上代碼
navs.vue
<template>
<div>
<!-- 內(nèi)容區(qū)域 -->
<div class="content">
<div>
content-0
</div>
<div>
content-1
</div>
<div>
content-2
</div>
<div>
content-3
</div>
<div>
content-4
</div>
</div>
<!-- 導(dǎo)航區(qū)域 -->
<ul class="navs">
<li :class="{active: active===0}">
content-0
</li>
<li :class="{active: active===1}">
content-1
</li>
<li :class="{active: active===2}">
content-2
</li>
<li :class="{active: active===3}">
content-3
</li>
<li :class="{active: active===4}">
content-4
</li>
</ul>
</div>
</template>
<script>
export default {
props: {},
data() {
return {
active: 0 // 當(dāng)前激活的導(dǎo)航索引
}
},
methods: {}
}
</script>
<style scoped>
/* 內(nèi)容區(qū)的樣式 */
.content {
background-color: white;
width: 500px;
}
.content div {
width: 100%;
height: 600px;
font-size: 36px;
padding: 20px;
background-color: #7ec384;
}
.content div:nth-child(2n) {
background-color: #847ec3;
}
/* 導(dǎo)航欄的樣式 */
.navs {
position: fixed;
top: 80px;
left: 700px;
background-color: #efefef;
}
.navs li {
padding: 0 20px;
line-height: 1.6;
font-size: 24px;
}
/* 當(dāng)導(dǎo)航被點亮后改變顏色 */
.navs .active{
color: #847ec3;
background-color: #e2e2e2;
}
</style>
布局很簡單,就左邊內(nèi)容,右邊導(dǎo)航。導(dǎo)航和內(nèi)容本來可以用 v-for 來生成的,但是為了看起來更簡單明了,我就沒那么寫。先來看看布局效果吧!
內(nèi)容導(dǎo)航聯(lián)動
這一塊開始變得有意思了起來,首先我們要搞懂元素的兩個屬性:
- scrollTop
- offsetTop
scrollTop
先看看一句話定義吧
一個元素的
scrollTop值是這個元素的頂部到視口可見內(nèi)容(的頂部)的距離的度量。當(dāng)一個元素的內(nèi)容沒有產(chǎn)生垂直方向的滾動條,那么它的scrollTop 值為0。
引用自 https://developer.mozilla.org/zh-CN/docs/Web/API/Element/scrollTop
如果還不理解,那么我畫張圖,大概就懂了,相信讀者也知道文檔流是個啥,我們先來個文檔流
好了,現(xiàn)在這個文檔流有點長,我們的視口不夠高,于是只能顯示一部分,其余的要滾動查看,看起來像這樣
這個時候我們看見的是最頂部的內(nèi)容,這個文檔流的 scrollTop 此時等于 0,讓我們把視口往下移一點,也就是滾動一下窗口,查看下面的內(nèi)容。
OK,到這里我相信你一定已經(jīng)理解了 scrollTop
offsetTop
還是先一句話定義吧
HTMLElement.offsetTop為只讀屬性,它返回當(dāng)前元素相對于其offsetParent元素的頂部內(nèi)邊距的距離。
引用自 https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/offsetTop
關(guān)于 offsetParent 網(wǎng)上有的定義是
與當(dāng)前元素最近的經(jīng)過定位( position 不等于 static )的父級元素
具體情況分為以下幾種:
position為fixed時,offsetParent為null,offsettop的值和top相等。此時元素是以視口來定位的。
position非fixed,父級元素?zé)o定位(static)時,offsetParent為body。
position非fixed,父級元素有定位時,offsetParent為最近的有定位的父級元素。
body元素,offsetParent為null,offsettop為0(似乎是廢話)。
引用自 http://m.itdecent.cn/p/135731ec13f1
我們這里是屬于第二種情況。
可能到這里你有點蒙,那還是拿剛才那張圖來舉例
好了,關(guān)于 offsetTop 你也了解了,我們就可以開始寫代碼了。
監(jiān)聽滾動
當(dāng)元素發(fā)生滾動時,會觸發(fā) scroll事件,我們就在 vue 的 mounted 鉤子中添加監(jiān)聽好了,修改 vans.vue 文件
<script>
export default {
props: {},
data() {
return {
active: 0 // 當(dāng)前激活的導(dǎo)航索引
}
},
mounted() {
// 監(jiān)聽滾動事件
window.addEventListener('scroll', this.onScroll)
},
destroy() {
// 必須移除監(jiān)聽器,不然當(dāng)該vue組件被銷毀了,監(jiān)聽器還在就會出錯
window.removeEventListener('scroll', this.onScroll)
},
methods: {
onScroll() {
// 滾動監(jiān)聽器
}
}
}
</script>
現(xiàn)在我們開始寫監(jiān)聽回調(diào)
// 滾動監(jiān)聽器
onScroll() {
// 獲取所有錨點元素
const navContents = document.querySelectorAll('.content div')
// 所有錨點元素的 offsetTop
const offsetTopArr = []
navContents.forEach(item => {
offsetTopArr.push(item.offsetTop)
})
// 獲取當(dāng)前文檔流的 scrollTop
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
// 定義當(dāng)前點亮的導(dǎo)航下標(biāo)
let navIndex = 0
for (let n = 0; n < offsetTopArr.length; n++) {
// 如果 scrollTop 大于等于第 n 個元素的 offsetTop 則說明 n-1 的內(nèi)容已經(jīng)完全不可見
// 那么此時導(dǎo)航索引就應(yīng)該是 n 了
if (scrollTop >= offsetTopArr[n]) {
navIndex = n
}
}
// 把下標(biāo)賦值給 vue 的 data
this.active = navIndex
}
到了這里,我們的導(dǎo)航已經(jīng)可以跟著滾動來變化了,看效果圖
用JS來滾動到指定元素
scrollTop 是可讀可寫的,我們只需要把目標(biāo)元素的 offsetTop 賦值給 scrollTop 就可以讓視口自動跳過去,但是直接跳過去是很生硬的,所以我們要一點一點的跳過去,只要速度夠快,人眼看上去就是動畫效果。好了,開始碼代碼。
給導(dǎo)航添加點擊點擊事件
<!-- 導(dǎo)航區(qū)域 -->
<ul class="navs">
<li :class="{active: active===0}" @click="scrollTo(0)">
content-0
</li>
<li :class="{active: active===1}" @click="scrollTo(1)">
content-1
</li>
<li :class="{active: active===2}" @click="scrollTo(2)">
content-2
</li>
<li :class="{active: active===3}" @click="scrollTo(3)">
content-3
</li>
<li :class="{active: active===4}" @click="scrollTo(4)">
content-4
</li>
</ul>
新增滾動函數(shù)
methods: {
...
// 跳轉(zhuǎn)到指定索引的元素
scrollTo(index) {
// 獲取目標(biāo)的 offsetTop
// css選擇器是從 1 開始計數(shù),我們是從 0 開始,所以要 +1
const targetOffsetTop = document.querySelector(`.content div:nth-child(${index + 1})`).offsetTop
// 獲取當(dāng)前 offsetTop
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
// 定義一次跳 50 個像素,數(shù)字越大跳得越快,但是會有掉幀得感覺,步子邁大了會扯到蛋
const STEP = 50
// 判斷是往下滑還是往上滑
if (scrollTop > targetOffsetTop) {
// 往上滑
smoothUp()
} else {
// 往下滑
smoothDown()
}
// 定義往下滑函數(shù)
function smoothDown() {
// 如果當(dāng)前 scrollTop 小于 targetOffsetTop 說明視口還沒滑到指定位置
if (scrollTop < targetOffsetTop) {
// 如果和目標(biāo)相差距離大于等于 STEP 就跳 STEP
// 否則直接跳到目標(biāo)點,目標(biāo)是為了防止跳過了。
if (targetOffsetTop - scrollTop >= STEP) {
scrollTop += STEP
} else {
scrollTop = targetOffsetTop
}
document.body.scrollTop = scrollTop
document.documentElement.scrollTop = scrollTop
// 屏幕在繪制下一幀時會回調(diào)傳給 requestAnimationFrame 的函數(shù)
// 關(guān)于 requestAnimationFrame 可以自己查一下,在這種場景下,相比 setInterval 性價比更高
requestAnimationFrame(smoothDown)
}
}
// 定義往上滑函數(shù)
function smoothUp() {
if (scrollTop > targetOffsetTop) {
if (scrollTop - targetOffsetTop >= STEP) {
scrollTop -= STEP
} else {
scrollTop = targetOffsetTop
}
document.body.scrollTop = scrollTop
document.documentElement.scrollTop = scrollTop
requestAnimationFrame(smoothUp)
}
}
}
}
}
最終局
好了,我們看看最終效果
最終代碼
<template>
<div>
<!-- 內(nèi)容區(qū)域 -->
<div class="content">
<div>
content-0
</div>
<div>
content-1
</div>
<div>
content-2
</div>
<div>
content-3
</div>
<div>
content-4
</div>
</div>
<!-- 導(dǎo)航區(qū)域 -->
<ul class="navs">
<li :class="{active: active===0}" @click="scrollTo(0)">
content-0
</li>
<li :class="{active: active===1}" @click="scrollTo(1)">
content-1
</li>
<li :class="{active: active===2}" @click="scrollTo(2)">
content-2
</li>
<li :class="{active: active===3}" @click="scrollTo(3)">
content-3
</li>
<li :class="{active: active===4}" @click="scrollTo(4)">
content-4
</li>
</ul>
</div>
</template>
<script>
export default {
props: {},
data() {
return {
active: 0 // 當(dāng)前激活的導(dǎo)航索引
}
},
mounted() {
// 監(jiān)聽滾動事件
window.addEventListener('scroll', this.onScroll, false)
},
destroy() {
// 必須移除監(jiān)聽器,不然當(dāng)該vue組件被銷毀了,監(jiān)聽器還在就會出錯
window.removeEventListener('scroll', this.onScroll)
},
methods: {
// 滾動監(jiān)聽器
onScroll() {
// 獲取所有錨點元素
const navContents = document.querySelectorAll('.content div')
// 所有錨點元素的 offsetTop
const offsetTopArr = []
navContents.forEach(item => {
offsetTopArr.push(item.offsetTop)
})
// 獲取當(dāng)前文檔流的 scrollTop
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
// 定義當(dāng)前點亮的導(dǎo)航下標(biāo)
let navIndex = 0
for (let n = 0; n < offsetTopArr.length; n++) {
// 如果 scrollTop 大于等于第n個元素的 offsetTop 則說明 n-1 的內(nèi)容已經(jīng)完全不可見
// 那么此時導(dǎo)航索引就應(yīng)該是n了
if (scrollTop >= offsetTopArr[n]) {
navIndex = n
}
}
this.active = navIndex
},
// 跳轉(zhuǎn)到指定索引的元素
scrollTo(index) {
// 獲取目標(biāo)的 offsetTop
// css選擇器是從 1 開始計數(shù),我們是從 0 開始,所以要 +1
const targetOffsetTop = document.querySelector(`.content div:nth-child(${index + 1})`).offsetTop
// 獲取當(dāng)前 offsetTop
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
// 定義一次跳 50 個像素,數(shù)字越大跳得越快,但是會有掉幀得感覺,步子邁大了會扯到蛋
const STEP = 50
// 判斷是往下滑還是往上滑
if (scrollTop > targetOffsetTop) {
// 往上滑
smoothUp()
} else {
// 往下滑
smoothDown()
}
// 定義往下滑函數(shù)
function smoothDown() {
// 如果當(dāng)前 scrollTop 小于 targetOffsetTop 說明視口還沒滑到指定位置
if (scrollTop < targetOffsetTop) {
// 如果和目標(biāo)相差距離大于等于 STEP 就跳 STEP
// 否則直接跳到目標(biāo)點,目標(biāo)是為了防止跳過了。
if (targetOffsetTop - scrollTop >= STEP) {
scrollTop += STEP
} else {
scrollTop = targetOffsetTop
}
document.body.scrollTop = scrollTop
document.documentElement.scrollTop = scrollTop
// 關(guān)于 requestAnimationFrame 可以自己查一下,在這種場景下,相比 setInterval 性價比更高
requestAnimationFrame(smoothDown)
}
}
// 定義往上滑函數(shù)
function smoothUp() {
if (scrollTop > targetOffsetTop) {
if (scrollTop - targetOffsetTop >= STEP) {
scrollTop -= STEP
} else {
scrollTop = targetOffsetTop
}
document.body.scrollTop = scrollTop
document.documentElement.scrollTop = scrollTop
requestAnimationFrame(smoothUp)
}
}
}
}
}
</script>
<style scoped>
/* 內(nèi)容區(qū)的樣式 */
.content {
background-color: white;
width: 500px;
}
.content div {
width: 100%;
height: 600px;
font-size: 36px;
padding: 20px;
background-color: #7ec384;
}
.content div:nth-child(2n) {
background-color: #847ec3;
}
/* 導(dǎo)航欄的樣式 */
.navs {
position: fixed;
top: 80px;
left: 700px;
background-color: #efefef;
}
.navs li {
padding: 0 20px;
line-height: 1.6;
font-size: 24px;
}
/* 當(dāng)導(dǎo)航被點亮后改變顏色 */
.navs .active{
color: #847ec3;
background-color: #e2e2e2;
}
</style>