实现抖音“刚刚看过”的功能(原生js的写法)
先上一下效果图吧
点击一下刚刚看过的按钮就会滚动到视频的位置
实现这个效果,如果不考虑效率问题肯定是非常简单,但是我们就是要考虑这个传输效率的问题
比方说这个主页有2000条视频,但是目前看的视频在第1900个,那需要滑到这第1900个视频的位置,不可能把之前所有的视频都加载出来吧,这样子的话这效率太低了吧,传输量加上请求,怎么可能吃得消
所以这个时候我们只需要创建好元素,但是不需要向服务器要这1900个视频的内容,我只要创建好元素,就可以滑动到这个视频的位置了,那要怎么加载这视频的内容呢?那就是判断用户看到哪一块,看到哪,我们加载到哪,类似于懒加载的效果
所以我这里提供一个思路,最主要的就是两个关键函数(createELement,loadPages)
createElement(page)的作用就是传入页码,他就会创建好这页面加上之前所有的元素,这个函数只管创建好元素,内容不归他管,内容等到后面在进行加载
loadPages()这个函数就是根据用户当前能看到第几页,那么就把第几页的内容加载出来,看到哪就加载哪个页面的数据,这里还需要考虑到两个页面重叠,都需要加载出来
那么首先来准备好html
<div class="contain"></div> //放置内容的盒子
<div class="btn"> //刚刚看过的按钮
<button class="full-rounded">
<span>刚刚看过</span>
<div class="border full-rounded"></div>
</button>
</div>
当然css样式也是要准备好的,可以根据自己公司的UI设计图来写
body {
background-color: #000;
padding: 100px 300px;
}
.contain {
width: 100%;
height: 100%;
display: grid; //宫格布局
grid-template-columns: repeat(5, 1fr);
grid-column-gap: 50px; //每一列的间距
grid-row-gap: 80px; //每一行的间距
}
.item {
width: 200px;
height: 300px;
border: 1px solid #fff;
}
.playing {
width: 200px;
height: 300px;
position: relative;
}
.playing img {
filter: blur(3px);
-webkit-filter: blur(3px);
}
.playing::after {
content: "播放中";
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 300px;
font-size: 20px;
font-weight: bold;
color: white;
display: flex;
justify-content: center;
align-items: center;
}
.btn {
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
}
button {
font-size: 16px;
position: relative;
margin: auto;
padding: 1em 2.5em 1em 2.5em;
border: none;
background: #fff;
transition: all 0.1s linear;
box-shadow: 0 0.4em 1em rgba(0, 0, 0, 0.1);
}
button:hover {
cursor: pointer;
}
button:active {
transform: scale(0.95);
}
button span {
color: #464646;
}
button .border {
position: absolute;
border: 0.15em solid #fff;
transition: all 0.3s 0.08s linear;
top: 50%;
left: 50%;
width: 9em;
height: 3em;
transform: translate(-50%, -50%);
}
button:hover .border {
display: block;
width: 9.9em;
height: 3.7em;
}
.full-rounded {
border-radius: 2em;
}
这些都不是最重要的
还有一些工具函数
1.getOffset(id) 来获取当前视频前面有多少个视频
这个根据实际情况来做,正常情况这里是向服务端获取的,我这里就模拟了一下请求
// 传入当前视频的id就可以获取之前有多少个视频
function getOffset(id) {
return new Promise((res, rej) => {
let result = id - 1;
res(result);
});
}
2.getVideo(page,size)
获取页面的资源
同样这里也是向服务端发请求获取的,我这里也是自己模拟
// 传入页码和每页多少条,即可获取图片数据
function getVideo(page, size) {
return new Promise((res) => {
let arr = [];
// 上一页有多少个,从哪开始num
let num = (page - 1) * size;
for (let i = 0; i < size; i++) {
let obj = {
id: num + i,
cover: `https://picsum.photos/200/300?id=${num + i}`,
};
arr.push(obj);
}
res(arr);
});
}
3.getIndexRange(page,size)
获取这个页码的最小索引和最大索引
// 传入页码和大小算出这个页码的起始和结束下标
function getIndexRange(page, size) {
let start = (page - 1) * size;
let end = start + size - 1;
return [start, end];
}
4.debounce(fn,deplay=300)
这个就是防抖啦,让loadpage函数不要执行太多次,节省性能
function debounce(fn, delay = 300) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
5.getPage(index,size)
传入当前视频的下标和页面大小,返回当前视频在第几页
function getPage(index, size) {
return Math.ceil((index + 1) / size);
}
以上都是工具函数
准备工作
1.定义好一页需要多少元素
const SIZE = 15;
// 刚刚看过视频的id
const currentId = 200;
// 页码
let i = 1;
2.获取页面两个重点元素
let contain = document.querySelector(".contain");
let btn = document.querySelector(".btn");
现在来写最重要的函数
1.createElement(page)
传入页码即可创建好这个页面包括之前的所有元素
步骤:
1.算出需要创建多少元素page*size
2.创建item添加到contain元素的children中
3.给每个item添加侦查器,判断是否出现在视口内
function createElement(page) {
// 防止一页重复创建
const childLen = contain.children.length;
const count = page * SIZE - childLen;
for (let i = 0; i < count; i++) {
const item = document.createElement("div");
item.className = "item";
item.dataset.index = i + childLen;
contain.appendChild(item);
ob.observe(item); //侦查器,判断是否出现在视口内
}
}
2.视口观察器
const visibleIndex = new Set(); //全局创建一个不重复的集合
let ob = new IntersectionObserver((entries) => {
for (const e of entries) {
const index = e.target.dataset.index;
//isIntersecting为true就代表在视口内
if (e.isIntersecting) {
visibleIndex.add(index);
} else {
visibleIndex.delete(index);
}
}
debounceLoadPage();// 防抖后的loadpage
});
3.获取集合的最大及最小的索引
function getRange() {
if (visibleIndex.size === 0) return [0, 0];
const max = Math.max(...visibleIndex);
const min = Math.min(...visibleIndex);
return [min, max];
}
4.加载视口内的元素的资源
function loadPage() {
// 得到当前能看到的元素索引范围
const [minIndex, maxIndex] = getRange();
const pages = new Set(); // 不重复的页码集合
for (let i = minIndex; i <= maxIndex; i++) {
pages.add(getPage(i, SIZE));// 遍历将侦查器集合范围内的所在页面都加入到pages的集合内
}
// 遍历页码集合
for (const page of pages) {
const [minIndex, maxIndex] = getIndexRange(page, SIZE);//获取页码的索引范围
if (contain.children[minIndex].dataset.loaded) { //如果页码最小索引的元素有自定义属性就跳过,代表加载过
continue;
}
contain.children[minIndex].dataset.loaded = true;//如果没有就代表没有加载过,添加上自定义属性
//将当前页码传给获取资源的函数
getVideo(page, SIZE).then((res) => {
//拿到当前页面需要的数据数组,遍历渲染到页面上
for (let i = minIndex; i < maxIndex; i++) {
const item = contain.children[i];
item.innerHTML = `<img src="${res[i - minIndex].cover}" alt="">`;
}
});
}
}
// 创建防抖加载函数,将loadpage函数防抖
const debounceLoadPage = debounce(loadPage, 300);
5.判断刚刚看过的按钮是否显示
// 页面进来就需要触发获取当前视频之前有多少个视频,判断按钮是否显示
async function setVisible() {
// 获取之前有多少个视频
let offest = await getOffset(currentId);
let [minIndex, maxIndex] = getRange();
// 返回告诉你第几页
const page = getPage(offest, SIZE);
if (offest >= minIndex && offest <= maxIndex) {
btn.style.display = "none";
} else {
btn.style.display = "block";
}
btn.dataset.page = page;
btn.dataset.index = offest;
}
6.给按钮添加点击事件,滚动到指定位置
btn.onclick = () => {
const page = +btn.dataset.page;
const index = +btn.dataset.index;
i = page; // 跳转将页码更新
createElement(page);
contain.children[index].scrollIntoView({
behavior: "smooth",
block: "center",
});
contain.children[index].classList.add("playing");
btn.style.display = "none";
};
7.给window添加滚动事件,页面触底页码加一
window.addEventListener("scroll", () => {
//窗口高度
var windowHeight =
document.documentElement.clientHeight || document.body.clientHeight;
//滚动高度
var scrollTop =
document.documentElement.scrollTop || document.body.scrollTop;
//页面高度
var documentHeight =
document.documentElement.scrollHeight || document.body.scrollHeight;
if (windowHeight + scrollTop == documentHeight) {
createElement(i++); //页面触底就页码加一
}
});
完整代码
<body>
<div class="contain"></div>
<div class="btn">
<button class="full-rounded">
<span>刚刚看过</span>
<div class="border full-rounded"></div>
</button>
</div>
<script src="./api.js"></script>
<script src="./index.js"></script>
<script>
const SIZE = 15;
let contain = document.querySelector(".contain");
let btn = document.querySelector(".btn");
// 页码
let i = 1;
const visibleIndex = new Set();
// 视口观察器
let ob = new IntersectionObserver((entries) => {
for (const e of entries) {
const index = e.target.dataset.index;
if (e.isIntersecting) {
// 将在视口内的元素添加到集合内
visibleIndex.add(index);
} else {
// 将不在视口内的元素从集合内删除
visibleIndex.delete(index);
}
}
debounceLoadPage();
});
function getRange() {
if (visibleIndex.size === 0) return [0, 0];
const max = Math.max(...visibleIndex);
const min = Math.min(...visibleIndex);
return [min, max];
}
// 创建元素
function createElement(page) {
// 防止一页重复创建
const childLen = contain.children.length;
const count = page * SIZE - childLen;
for (let i = 0; i < count; i++) {
const item = document.createElement("div");
item.className = "item";
item.dataset.index = i + childLen;
contain.appendChild(item);
ob.observe(item);
}
}
// 得到当前能看到的元素索引范围
function loadPage() {
const [minIndex, maxIndex] = getRange();
const pages = new Set();
for (let i = minIndex; i <= maxIndex; i++) {
pages.add(getPage(i, SIZE));
}
for (const page of pages) {
const [minIndex, maxIndex] = getIndexRange(page, SIZE);
if (contain.children[minIndex].dataset.loaded) {
continue;
}
contain.children[minIndex].dataset.loaded = true;
getVideo(page, SIZE).then((res) => {
for (let i = minIndex; i < maxIndex; i++) {
const item = contain.children[i];
item.innerHTML = `<img src="${res[i - minIndex].cover}" alt="">`;
}
});
}
}
// 创建防抖加载函数
const debounceLoadPage = debounce(loadPage, 300);
// 刚刚看过视频的id
const currentId = 200;
// 页面进来就需要触发获取之前有多少个视频,判断按钮是否显示
async function setVisible() {
// 获取之前有多少个视频
let offest = await getOffset(currentId);
let [minIndex, maxIndex] = getRange();
// 返回告诉你第几页
const page = getPage(offest, SIZE);
if (offest >= minIndex && offest <= maxIndex) {
btn.style.display = "none";
} else {
btn.style.display = "block";
}
btn.dataset.page = page;
btn.dataset.index = offest;
}
btn.onclick = () => {
const page = +btn.dataset.page;
const index = +btn.dataset.index;
i = page;
createElement(page);
contain.children[index].scrollIntoView({
behavior: "smooth",
block: "center",
});
contain.children[index].classList.add("playing");
btn.style.display = "none";
};
window.addEventListener("scroll", () => {
//窗口高度
var windowHeight =
document.documentElement.clientHeight || document.body.clientHeight;
//滚动高度
var scrollTop =
document.documentElement.scrollTop || document.body.scrollTop;
//页面高度
var documentHeight =
document.documentElement.scrollHeight || document.body.scrollHeight;
if (windowHeight + scrollTop == documentHeight) {
createElement(i++);
}
});
createElement(i);
setVisible();
</script>
</body>
🔥🔥🔥🔥🔥🔥到这里就实现了抖音的刚刚看过的功能!!!!!🔥🔥🔥🔥🔥🔥🔥🔥🔥
作者:井川不擦
来源:juejin.cn/post/7257441472445644855
来源:juejin.cn/post/7257441472445644855