实现抖音“刚刚看过”的功能(原生js的写法)

先上一下效果图吧

点击一下刚刚看过的按钮就会滚动到视频的位置
image.png

image.png

实现这个效果,如果不考虑效率问题肯定是非常简单,但是我们就是要考虑这个传输效率的问题

比方说这个主页有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}" >`;
            }
          });
        }
      }
      
       // 创建防抖加载函数,将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}" >`;
            }
          });
        }
      }

      // 创建防抖加载函数
      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>

??????到这里就实现了抖音的刚刚看过的功能!!!!!?????????

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYVEU5Nw' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片