瀑布流组件陷入商品重复怪圈?我是如何用心一解的!

news2024/11/26 20:46:12

目录

背景

瀑布流组件

什么是瀑布流组件

如何实现一个瀑布流组件

商品重复的原因

解决方案

方法一(复杂,不推荐):标记位大法

方法二(优雅,推荐):Promise + 队列 大法

总结


背景

        某天我们公司小程序收到线上反馈,在商品列表页面为什么我划着划着划着,就会出现一些重复商品......

        在讲这个问题之前,先讲一下我们是如何实现瀑布流组件的

瀑布流组件

什么是瀑布流组件

如图所示下方商品列表就采用了瀑布流的布局,视觉表现为参差不齐的多栏布局。

如何实现一个瀑布流组件

        下面简单写一下实现瀑布流的思路,左右两列布局,根据每一列的高度来判断下次插入到哪一列中,每次插入列中需重新计算高度,将下一个节点插入短的哪一列中,如下图所示:

下面代码示例(仅展示思路)

// dataList 就是我们整个的商品卡片列表的数据 ,用户滑动到底部会加载新一页的数据 会再次触发 watch
watch(() => 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 <= rightHeight ? leftDataList.value.push(item) : rightDataList.value.push(item); //判断两边高度,来决定添加到那边
    // 渲染dom
    await nextTick();
    // 获取dom渲染后的 左右两边的高度
    leftHeight = await getViewHeight('#left')
    rightHeight = await getViewHeight('#right')
  }
  lastIndex.value = newList.length
}
<template>
  <view>
    <view id="left">xxxx</view>
    <view id="right">xxxx</view>
  </view>
</template>

        当用户滚动到底部的时候会加载下一页的数据,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 <= rightHeight ? leftDataList.value.push(item) : rightDataList.value.push(item); //判断两边高度,来决定添加到那边
    // 等待dom渲染完成
    await nextTick();
    // 获取dom渲染后的 左右两边的高度
    leftHeight = await getViewHeight('#left')
    rightHeight = await getViewHeight('#right')
    // 判断是最后一个节点
    if((tempList.length - 1) === i) {
      fallLoad.value = true
    }
  }
  lastIndex.value = newList.length
}

        这样的话会丢弃掉用户快速滑动时触发的 dataRender ,只有在 DOM 渲染完成后再次触发新的请求时才会再次触发。但是这样可能会存在另外一个问题,有部分的 dataRender 被丢弃掉了,同时用户把所有的数据都加载完成了,没有新的数据来触发 watch ,这就导致部分商品的数据准备好了但在页面上没有渲染,因此我们还需要针对这种情况再去做单独处理, ,我们可以额外加一个状态来判断 rightDataList + leftDataList 的总数是否等于 dataList,不等于的时候可以再触发一次 dataRender ......

        其实我们这种场景其实已经不太适合用标记位大法,强行使用只会让代码变成一座“屎山”,但是其实在我们日常业务中,添加标记位是一种很实用的方法,比如给某个按钮添加 loading ,防止某些事件、请求频繁执行等。

方法二(优雅,推荐):Promise + 队列 大法

        由于我们并不能丢弃异常情况触发的 dataRender, 那我们只能让 dataRender 有序的执行。

        我们重新整理思路,首先我们先把复杂的问题简单化。抛开我们的业务场景,dataRender 就可以当做一个异步的请求,然后问题就变成了在同一时间我们收到了多个异步的请求,我们怎么让这些异步请求自动、有序执行。

经过上面的推导我们拆解出以下几个关键点:

  1. 我们需要一个队列,队列中存储每个异步任务

  2. 当把这个任务添加到这个队列中的时候自动执行第一个任务

  3. 我们需要使用 promise.then() 来保证任务有序的执行

  4. 当存队列中在多个异步任务的时候,怎么在执行完成第一个之后再去自动的执行后续的任务

        第一次执行的时机其实我们是知道,那我们需要现在解决的问题是执行完成第一个后怎么去自动执行后续的请求?

  1. 使用循环,可参考瀑布流组件中的 for await of 确保每次异步任务的执行,这里就不过多阐述了,这么写代码不太优雅

  2. 使用递归,在每个 promise.then 中递归下一个 promise

通过这几点关键点我们写出使用递归的方案的代码:

class asyncQueue {
  constructor() {
    this.asyncList = [];
    this.inProgress = false;
  }

  add(asyncFunc) {
    return new Promise((resolve, reject) => {
      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
})

        通过上述代码我们就可以,让我们的每一个异步任务有顺序的执行,并且让每一个异步任务执行完成以后自动执行下一个,完美的达到了我的需求。

        其实这个方法不仅适用于当前场景,我们很多的业务场景都会遇到这种情况,会被动接受多个请求,但是这些请求还要有序的执行,我们都可以使用这种方法。

下面我简单列举了两种其他的场景:

  1. 比如某个按钮用户点击了多次,但是我们要让这些请求有序的执行并且依次拿到这些请求返回的数据

  2. 某些高频的通信操作,我们不能丢弃用户的每次通信,而是需要用这种队列的方式,自动、有序的执行

总结

        上述的这些“点” ,标记位、promise、队列、递归等,在日常开发中几乎充斥在我们项目的每一个角落,但是如何使用好这些”点“值得我们深思的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/538561.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决新思路#报错:ping: www.baidu.com: 未知的名称或服务--照着步骤来还是ping不通的可能原因

最近由ubantu转为centos7,配置hadoop&#xff0c;配置静态ip的过程中一直ping不通。之前配置ubantu的也是&#xff0c;终于这次在重启虚拟机和主机后发现了原因。 中途尝试过: 1.三次以上命令行反复操作 2.图形界面设置 3.看是否主机的网络适配器的网关与设置的IP地址产生冲突…

JavaScript实现计算100之间能被5整除的数的代码

以下为实现计算100之间能被5整除的数的程序代码和运行截图 目录 前言 一、计算100之间能被5整除的数 1.1 运行流程及思想 1.2 代码段 1.3 JavaScript语句代码 1.4 运行截图 前言 1.若有选择&#xff0c;您可以在目录里进行快速查找&#xff1b; 2.本博文代码可以根据题…

2023最新100道渗透测试面试题(附答案)

眨眼间2023年快过去一半了&#xff0c;不知道大家有没有找到心仪的工作呀&#xff0c;今天我给大家整理了100道渗透测试面试题给大家&#xff0c;需要答案的话可以在评论区给我留言哦~ 第一套渗透面试题 什么是渗透测试&#xff1f;它的目的是什么&#xff1f; 渗透测试的五个…

DirectX12 简单入门(一)

在很久以前写过关于DirectX9的一些应用&#xff0c;直到现在DirectX12已经普及了。写完几个DirectX12测试代码之后&#xff0c;写一篇DirectX12简单入门介绍一下基本概念&#xff0c;以及环境搭建和编程过程。 编程环境 与DirectX9不同&#xff0c;在DirectX12开发中微软将需…

『MySQL 实战 45 讲』“order by” 是怎么工作的

“order by” 是怎么工作的 首先创建一个表 CREATE TABLE t ( id int(11) NOT NULL, city varchar(16) NOT NULL, name varchar(16) NOT NULL, age int(11) NOT NULL, addr varchar(128) DEFAULT NULL, PRIMARY KEY (id), KEY city (city) ) ENGINEInnoDB;全字段排序 在 cit…

自己搭建go web 框架

思想base部分day1:封装gee封装context上下文封装tree路由树分组封装group与中间件封装文件解析封装封装错误处理 思想 web框架服务主要围绕着请求与响应来展开的 搭建一个web框架的核心思想 1 便捷添加响应路径与响应函数(base) 2 能够接收多种数据类型传入(上下文context) 3 构…

【Linux】Linux入门学习之常用命令五

介绍 这里是小编成长之路的历程&#xff0c;也是小编的学习之路。希望和各位大佬们一起成长&#xff01; 以下为小编最喜欢的两句话&#xff1a; 要有最朴素的生活和最遥远的梦想&#xff0c;即使明天天寒地冻&#xff0c;山高水远&#xff0c;路远马亡。 一个人为什么要努力&a…

支付系统设计五:对账系统设计01-总览

文章目录 前言一、对账系统构建二、执行流程三、获取支付渠道数据1.接口形式1.1 后台配置1.2 脚本编写1.2.1 模板1.2.2 解析脚本 2.FTP形式2.1 后台配置2.2 脚本编写2.2.1 模板2.2.2 解析脚本 四、获取支付平台数据五、数据比对1. 比对模型2. 比对器 总结 前言 从《支付系统设…

AE基础教程

一&#xff1a;粒子插件。 AEPR插件-Trapcode Suite V18.1.0 中文版 二&#xff1a;跟随手指特效。 1&#xff1a;空对象位置关键帧跟随手指。 2&#xff1a;发射粒子位置&#xff0c;按住Alt键&#xff0c;连接到空对象位置处。。 三&#xff1a;CtrI导入素材快捷键。 四&a…

Elasticsearch基础学习-常用查询和基本的JavaAPI操作ES

关于ES数据库的和核心倒排索引的介绍 一、Elasticsearch概述简介关于全文检索引擎关系型数据库的全文检索功能缺点全文检索的应用场景Elasticsearch 应用案例 二、Elasticsearch学习准备安装下载关于es检索的核心-倒排索引正向索引&#xff08;forward index&#xff09;倒排索…

辅助驾驶功能开发-功能规范篇(16)-2-领航辅助系统NAP-自动变道-1

书接上回 2.3.4.自动变道 当车辆处于导航引导模式NOA功能时(即车辆横向控制功能激活),且车速大于40km/h,驾驶员按下转向灯拨杆或系统判断当前有变道需要时,自动变道系统通过对车道线、自车道前方目标距离、邻近车道前后方目标距离等环境条件进行判断,在转向灯亮起3s后控…

看到这个数据库设计,我终于明白了我和其他软测人的差距

测试人员为什么要懂数据库设计&#xff1f; 更精准的掌握业务&#xff0c;针对接口测试、Web 测试&#xff0c;都是依照项目/产品需求进行用例设计&#xff0c;如果掌握数据库设计知识&#xff0c;能直接面对开发的数据表&#xff0c;更好、更精准的理解业务逻辑&#xff1b;有…

【滑动窗口】滑窗模板,在小小的算法题里滑呀滑呀滑

首先大家先喊出我们的口号&#xff1a;跟着模板搞&#xff0c;滑窗没烦恼&#xff01; 一.什么是滑动窗口&#xff1f; 滑动窗口算法是双指针算法的一种特定化的算法模型&#xff0c;常用于在特定的条件下求最大或者最小的字符串&#xff0c;特定的数组&#xff0c;以及字符序列…

JAVA 可用的高性能docker镜像及如何使用?

目前docker hub上下载量很大的java、openjdk镜像都已经被弃用,不再维护,目前可用的java docker镜像有哪一些呢?哪一些镜像是主流的? 本文带有领略java可用的镜像资源、如何使用它们,如何构建springboot镜像? 1. 可用的java镜像 1.1. amazoncorretto 1.1.1. 什么是Corr…

环路详解:交换机环路产生的过程和原因图解

前言&#xff1a; 在了解环路之前得先了解交换机的工作原理&#xff0c;当然交换机的基本工作原理其实非常简单&#xff0c;只有“单播转发与泛洪转发”、“交换机MAC地址表”这两个&#xff01;其他的如vlan&#xff0c;生成树等也是在此基础上增加的&#xff0c;弥补交换机基…

node笔记_koa框架的路由

文章目录 ⭐前言⭐koa 原生路由写法⭐引入 koa-router&#x1f496; 安装koa-router&#x1f496; 动态读取路径文件作为路由 ⭐结束 ⭐前言 大家好&#xff0c;我是yma16&#xff0c;本文介绍koa框架的路由。 往期文章 node_windows环境变量配置 node_npm发布包 linux_配置no…

[网络安全]DVWA之XSS(Reflected)攻击姿势及解题详析合集

[网络安全]DVWA之XSS&#xff08;Reflected&#xff09;攻击姿势及解题详析合集 XSS(Reflected)-low level源代码姿势 XSS(Reflected)-medium level源代码姿势1.双写绕过2.大小写绕过 XSS(Reflected)-high level源代码str_replace函数 姿势 XSS(Reflected)-Impossible level源代…

ssh正反隧道(代理msf对icmp穿透监听)

ssh正向隧道&#xff1a; 就是将本地端口映射到远程上&#xff0c;相当访问本地端口就是访问远程的端口 正向 访问本地对应的是远程的端口 ssh -fNCL 本地ip:本地port:远程ip:远程port 用户远程ip/域名 实例&#xff1a; ssh -fNCL 192.168.222.128:90:192…

HTML的表单

前后端交互过程&#xff1a; 表单在 Web 网页中用来给访问者填写信息采集客户端信息&#xff0c;使网页具有交互的功能&#xff0c;用户填写完提交后&#xff0c;表单的内容就从客户端的浏览器传送到服务器上&#xff0c;经过服务器上程序处理后&#xff0c;再将用户所需信息传…

人机大战?——带你玩转三子棋(C语言)

TOC 1、前言 在学习完数组之后&#xff0c;我们就可以自己来实现一个简单游戏—三子棋了&#xff01; 为了确保程序的独立性&#xff1a;我们创建了一个源函数game.c 和test.c&#xff0c;一个头文件game.h test.c——测试游戏 game.c——游戏函数的实现 game.h——游戏函数…