前言: 前两天接到了一个需要,次要性能是实现相似于 B 站 音讯页面的那种成果,右侧几个 tab 都须要应用到有限加载的性能。
大家都晓得,程序员是很懒的,不可能这几个页面全都写一遍反复的逻辑。所以在接到这个需要的时候,就开始思考能不能设计一个通用的函数,能够帮我疾速的实现这个需要。🤔
一. 什么是有限列表
- 如果你之前不理解什么是 有限列表,然而我敢保障你肯定在不知情的状况下体验过这个。咱们还拿 B 站 举例子。如下图:
你会发现,只有当你往下滚动后,这个页面的数据才会一直被填充,如果你留心的话,你会留神到右侧的滚动条会在快靠近尾部的时候,又一次滑动到下面一点间隔,那是因为前面那一部分数据是之后才加进来的。
接下来让咱们模仿一下这个场景,来通知你这样设计的益处。
-
当用户点击左侧《零碎音讯》按钮的时候,假如当初数据库有 100 条音讯,那么咱们有两个计划能够抉择:
-
- 后端间接返回 100 条数据
-
- 后端只把最后面最新的数据返回
-
- 很不言而喻,站在用户的角度来讲,其实咱们最心愿点进来后,首屏幕显示的是最新的那几条音讯,其实之前过老的音讯我不是特地关怀,然而咱们又不能把老信息齐全不给用户看。(万一用户有一个中奖音讯被忘记到了前面,用户某天忽然想起来怎么办呢?)
- 那么这时候咱们就须要将下面两种形式联合起来,咱们能够假如用户刚进来此页面,咱们临时先只返回最新的前 10 条数据给用户,一旦当用户滚动到最下方时,咱们就判断用户是想查阅更为之前时间段的音讯,那么咱们再返回后 10 条给用户。如果用户再向下滚动,咱们再返回 10 条 … 以此类推,直到 100 条数据全副返回。
- 领会到了下面的场景,我想你应该曾经了解了有限列表存在的意义。
二. 传统有限列表
-
在入手解决这个需要之前,我参考了网上之前曾经有的现成的轮子和大部分有限列表的设计思路,发现了很多都是通过观察 scroll 事件来判断容器是否曾经滚动到底部。
- 如果到底:发动申请,将新取得的数据 push 到数组中。
- 没到底:不做任何解决。
- 咱们写一个简略的 demo 来模仿一下这个场景。简略来讲就是一个容器 div 放了过多的内容,导致了溢出,并且咱们设置了 overflow-auto , 使容器能够向溢出的方向滚动。
这里我为了不便拆穿,创立了一个长度为 10 的空数组,而后通过 v-for 去渲染,每一个 div 固定一个长度 100 px。渲染的后果如下:
- 依据下面咱们刚刚提到的有限列表的逻辑,当初咱们须要判断用户什么时候“滚到底了”。这里第一步须要给以后的容器元素绑定 scroll 事件。
这样咱们就能够获取到容器元素的 scrollTop 属性。
-
可能你还会有疑难🤔,拿到 scrollTop 有什么用呢?的确,咱们单单晓得这一个属性是没有什么用的,咱们须要搭配应用另外两个非常重要的属性一起应用,才能够达到咱们的目标。那就是
clientHeight
还有scollHeight
。这里有一个触底计算方法。clientHeight + scrollTop = scrollHegiht
-
最开始看到这个计算方法的时候,我也很蛊惑,为什么这样就示意到底了呢?这几个属性我之前的文章里有具体解释。
🎁你必须晓得的 clientWidth, offsetWidth, scrollWidth.
如果你懒得看,没关系,我接下来会简略形容一下,不会影响你进一步浏览本文的主主体内容。
- clientHeight 代表咱们容器 内容区域 的高度。更加直观来讲,当你元素溢出了,并且你设置一个 overflow-hidden,那么疏忽溢出的内容,你能够间接看到的区域就是 clientHeight,也就是这一部分的高度。
- scrollTop 代表咱们容器向下滚动了多少高度。这里为了更好的体现出 scrollTop,咱们在控制台输入一下。能够看到 scrollTop 随着咱们向下滚动,值越来越大。
- scrollHeight 其实代表着这个元素 理论 的高度,因为人家原本就这么高,只不过之前你给容器设置了
overflow-auto
,把人家的高度给暗藏了一部分,当初还给人家了而已。
为了更直观的看到这个属性的含意,咱们把容器的
overflow-auto
设置为overflow-visible
。咱们验证一下,咱们已晓得每个元素的高度是 100px,当初有 10 个元素,那么如果咱们推断的没错,那么 scroll 的值应该
100px * 10 = 1000px
。让咱们选中这个容器高度,在选项卡中搜寻scrollHeight
,能够看到咱们的猜测没错,它代表的就是理论高度。 -
大略理解了这三个属性的含意,那么咱们回过头再来看咱们的触底公式。
clientHeight + scrollTop = scrollHegiht
在这里你须要理清一个十分重要的细节,咱们的 scrollTop 的值是有极限的,即便你滚动到底了,那么还是会有一个可视区域的高度在你眼前,它是不可能滚动到最初一个元素也看不见的。如下图:
当第 10 个元素呈现的时候,其实你曾经无奈滚动了,此时的 scrollTop 就是最大值。也代表着不可见元素(被暗藏的元素)的总高度。
- 想分明下面这个细节,咱们就能够反推出当容器滚动到底的状况,(不可见元素高度
scrollTop
),加上以后可视区域的高度(clientHeight
) 不正好就是理论的总高度嘛!(scrollHeight
)。此时正好对上了咱们的触底公式,此时也正是在底部的时候。 - 依据下面的触底公式,咱们很容易的能够写出上面的判断逻辑。
让咱们验证一下是否可行。
- 为了模仿更实在的状况,咱们在触底的时候,扭转数组的长度。
再来看一下成果
能够看到咱们的数组长度从原本的 10,变为了 20。
- 随着滚动,反复上述步骤,其实就是传统有限列表的实现原理。然而咱们大家都晓得,获取
clientHeight
等这些属性浏览器为了保障拿到最新的数据是会引起重绘的,并且 scroll 事件触发的频率极高,然而这个场景下又不能做节流和防抖。那有没有更好的解决办法呢?🤔
三. 转变思路
- 咱们把之前 scroll 相干的函数和属性都去掉,接下来咱们在容器元素内加上一个 垫底元素,说白了,就是容器元素的最初一个子元素。
当初的款式大略是这样的:
天经地义的滚动到底部,就会看到咱们的垫底元素。
- 那咱们的思路是否就能够从判断元素的 触底公式 转变为 => 什么时候看到垫底元素 了呢?那怎么判断能力优化浏览器的事件还能完满达成咱们的有限列表加载呢?
- 接下来引入咱们明天的配角,IntersectionObserver,你能够间接翻译中文 —-穿插观察者。
四. IntersectionObserver API
-
具体的细节的介绍,你能够点击下方查看,在文中我只会介绍这个 API 的外围性能。不过我还是强烈建议你先查阅当前再开观看,能让你更深刻了解本文的思维。(T.T 真不是我懒,真的是阮大讲的太好了,我就不再献丑了,我只把我的设计思维通知大家,置信大家都是很聪慧的!)
- MDN IntersectionObserver
- 阮一峰 intersectionObserver 教程
- 首先你必须晓得的一点,这个 api 是一个构造函数,能够承受一个函数作为参数。所以你第一步的应用形式应该像上面这样。
- 在此之前,咱们先做一下筹备工作。
你须要在实在元素挂载当前,调取 observer 实例对象身上的 observe 办法,它接管一个实在 dom 作为参数。这里咱们把 垫底元素 放进去察看,具体怎么个观察法,咱们接下来会讲到。
- 当你胜利开始察看时,你的回调函数会被触发,能够在管制台上打印一下咱们回调函数的参数,能够看到一个叫做 IntersectionObserverEntry的类型变量。为什么是数组呢?因为这个 api 容许你同时察看多个元素,所以这个参数才是数组。
5. 接下来咱们重点就是要去解决回调函数里的逻辑,在这里我间接讲重点,因为咱们只察看了一个元素,所以咱们的回调函数的参数重,垫底元素就是 entries[0]
。咱们控制台打印一下这个变量。
能够看到这个变量身上有很多属性。
- 这里我间接讲重点,咱们临时只须要关怀这个
intersectionRatio
的值。这个值代表着 垫底元素 和视口元素 的穿插比例。你能够临时简略的了解整个文档的根元素。 - 当咱们页面没有产生滚动时,咱们假如红色方块为咱们被暗藏了的垫底元素,当初你的视线里是没有它的,所以穿插比例为 0。
- 当咱们滚动到底部的时候,这时候的穿插比例就是 100%,也就是 1。然而在这里你须要用到这个 api 的第二个参数能力看到这个状况。
让咱们设置一个叫做 threshold 的属性值(阈值 )这样你就能够指定 达到穿插的比例 时再触发回调函数。
通过上面的 gif 图能够看到,只有当咱们元素齐全呈现的时候,才会触发回调函数。(tips:第一次打印是因为这个 api 初始化的时候会默认执行一次。)
- 那么接下来的我置信你应该明确我的意思了,咱们只须要在穿插比例为 1 的时候,去发动申请即可。
让咱们看一下成果:
能够看到,咱们曾经完满复刻了传统的有限列表计划,并且这个 api 是异步执行的,只会在主过程闲下来的时候再执行回调函数,防止了咱们手动优化 scroll 事件带来的负面影响。
五. 设计一个通用函数
- 咱们再回过头看一下 B 站的左侧 tab,会发现这几个页面都是很相似的,所以咱们能够设计一个函数来封装一个通用的 IntersectionObserver 函数。
- 你能够搭配 题目六 来观看本大节,首先这个函数会返回一个 init 函数 和一个响应式变量 list。
- 接下来我解说一下我的设计思路。
- 首先这个函数须要接管一个函数作为参数,这个参数就是你每个页面去申请后端的那个函数。我在函数外部封装了一个叫做 fetchData 的函数,它会在某些条件上来申请后端,一直填充咱们的
list
变量。 - 外围函数其实就是 init,咱们须要借助 vue3 组合式 api,来封装好它。留神,这个 init 须要接管一个容器元素作为参数,因为须要给这个传进来的容器元素增加 垫底元素 来判断是否曾经滚动到底部了。
- 首先第一次加载的时候,咱们须要默认填充一次咱们的 list。
- 而后咱们在 nextTick 里去动静增加一个 垫底元素。
- 紧接着开启 观察者 API 来判断穿插比例,如果为 1,那么调取 fetchData 函数 填充咱们的 list 即可。
- 接下来你只须要在每个须要用到的页面里去调取这个函数即可。
六. 源码
import {ref, nextTick} from "vue";
export function useInfiniteLoad(fetchListFn: () => Promise<any[]>) {const data = ref<any[]>();
const list = ref<any[]>([]);
async function fetchData() {data.value = await fetchListFn();
list.value.push(...data.value);
}
// observerFn
async function init(containerEl: HTMLElement) {await fetchData();
if (!containerEl) return;
await nextTick(() => {const dom = document.createElement("div");
dom.setAttribute("id", "loadmore");
dom.style.height = "1px";
dom.textContent = " ";
containerEl.appendChild(dom);
const observer = new IntersectionObserver((entries: IntersectionObserverEntry[]) => {const ratio = entries[0].intersectionRatio;
if (ratio === 1) {fetchData();
}
}
);
observer.observe(dom);
});
}
return {init, list};
}
七. 结语
这个函数仅仅只是启发你的设计思路,并不能在理论我的项目中齐全满足你的需要,我在我的项目中用到的函数其实是依据咱们后端分页设计来欠缺的,然而总体的思维是不变的,你须要做的依据我的项目来封装你学到的内容。🎁