|
瀑布流组件陷入商品重复怪圈?我是如何用心一解的!
324 瀑布流组件陷入商品重复怪圈?我是如何用心一解的!http://zoo.zhengcaiyun.cn/blog/article/waterfall-flow背景某天我们公司小程序收到线上反馈,在商品列表页面为什么我划着划着划着,就会出现一些重复商品......Alt text在讲这个问题之前,先讲一下我们是如何实现瀑布流组件的瀑布流组件什么是瀑布流组件如图所示下方商品列表就采用了瀑布流的布局,视觉表现为参差不齐的多栏布局。Alt text如何实现一个瀑布流组件下面简单写一下实现瀑布流的思路,左右两列布局,根据每一列的高度来判断下次插入到哪一列中,每次插入列中需重新计算高度,将下一个节点插入短的哪一列中,如下图所示:Alt textAlt text下面代码示例(仅展示思路)// dataList 就是我们整个的商品卡片列表的数据 ,用户滑动到底部会加载新一页的数据 会再次触发 watchwatch(() => props.dataList ,(newList) => { dataRender(newList)},{ immediate: true,})const dataRender = async (newList) => { // 获取左右两边的高度 let leftHeight: number = await getViewHeight('#left') let rightHeight: number = await getViewHeight('#right') // 取下一页数据 const tempList = newVal.slice(lastIndex.value, newVal.length) for await (const item of tempList) { leftHeight xxxx xxxx 当用户滚动到底部的时候会加载下一页的数据,dataList 会发生变化,组件会监听到 dataList 的变化来执行 dataRender,dataRender 中会去计算左右两列的高度,哪边更短来插入哪边,循环 list 来完成整个列表的插入。商品重复的原因乍一看上面代码写的很完美,但是却忽略 DOM 渲染还需要时间,代码中使用了 for await 保证异步循环有序进行,并且保证数据变化后 DOM 能渲染完成后获取到新的列高,这样却导致了 DOM 渲染的比较慢。DOM 在没有加载完成的情况下,用户再次滑动到底部会再次加载新的一页数据,导致 watch 又会被触发,dataRender 会再次被执行,相当于会存在多个 dataRender 同时在执行。但是 dataRender 中使用到了全局的 leftDataList、rightDataList 和 lastIndex ,如果多个 dataRender 同时执行的话就会到数据错乱,lastIndex 错乱会导致商品重复,leftDataList 和 rightDataList 错乱会导致顺序问题。下面用伪代码讲述一下之间的关系// 正常情况代码会像如下情况去走list = [1,2,3,4,5]// 数组执行完成后lastIndex = 5// 加载下一页数据后 list = [1,2,3,4,5,6,7,8,9,10]list.slice(lastIndex, list.length) // [6,7,8,9,10]但是如果 dataRender 同时执行 大家都共用同一个 lastIndex ,lastIndex 并不是最新的,就会变成下面这种情况list.slice(lastIndex, list.length) // [1,2,3,4,5,6,7,8,9,10]同理顺序错乱也是这种情况解决方案出现这个问题的原因是存在多个 dataRender 同时执行,那我们只需想办法在同一时间只能有一个在执行就可以了。方法一(复杂,不推荐):标记位大法看着这个方法相信大部分人经常把它用作防抖节流,例如不想让某个按钮频繁点击导致发送过多的请求、点击的时候让某个请求完全返回结果后才能再次触发下次请求等。因此我们这里的思路也是控制异步任务的次数,在一个 dataRender 完全执行完成之后才能执行另一个 dataRender ,在这里我们首先添加一个全局标记 fallLoad, 在最后一个节点渲染完才可以执行 dataRender,代码改造如下const fallLoad = ref(true)watch(() => { if(fallLoad.value) { dataRender() fallLoad.value = false }})const dataRender = async () => { let i = 0 const tempList = newVal.slice(lastIndex.value, newVal.length) for await (const item of tempList) { i++ leftHeight { this.asyncList.push({asyncFunc, resolve, reject}); if (!this.inProgress) { this.execute(); } }); } execute() { if (this.asyncList.length > 0) { const currentAsyncTask = this.asyncList.shift(); currentAsyncTask.asyncFunc() .then(result => { currentAsyncTask.resolve(result); this.execute(); }) .catch(error => { currentAsyncTask.reject(error); this.execute(); }); this.inProgress = true; } else { this.inProgress = false; } }}export default asyncQueue每次调用 add 方法会往队列中添加经过特殊包装过的异步任务,并且只有在只有在没有正在执行中的任务的时候才开始执行 execute 方法。在每次执行异步任务时会从队列中 shift ,利用 promise.then 并且递归调用该方法,实现有序并且自动执行任务。在封装在这方法的过程中同样也使用到了我们的标记位大法 inProgress ,来保证我们正在执行当前队列时,突然又进来新的任务而导致队列执行错乱。调用方法如下:const queue = new asyncQueue()watch(() => props.dataList, async (newVal, oldVal) => { queue.add(() => dataRender(newVal))}, { immediate: true, deep: true})通过上述代码我们就可以,让我们的每一个异步任务有顺序的执行,并且让每一个异步任务执行完成以后自动执行下一个,完美的达到了我的需求。其实这个方法不仅适用于当前场景,我们很多的业务场景都会遇到这种情况,会被动接受多个请求,但是这些请求还要有序的执行,我们都可以使用这种方法。下面我简单列举了两种其他的场景:比如某个按钮用户点击了多次,但是我们要让这些请求有序的执行并且依次拿到这些请求返回的数据某些高频的通信操作,我们不能丢弃用户的每次通信,而是需要用这种队列的方式,自动、有序的执行总结上述的这些“点” ,标记位、promise、队列、递归等,在日常开发中几乎充斥在我们项目的每一个角落,但是如何使用好这些”点“值得我们深思的。,包含前端、后端、测试、UED 等,Base 在风景如画的杭州,一个富有激情和技术匠心精神的成长型团队。前端,隶属于研发部。团队现有 90 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。
|
|