自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

瀑布流組件陷入商品重復(fù)怪圈?我是如何用心一解的!

開發(fā) 前端
這個方法不僅適用于當(dāng)前場景,我們很多的業(yè)務(wù)場景都會遇到這種情況,會被動接受多個請求,但是這些請求還要有序的執(zhí)行,我們都可以使用這種方法。

背景

某天我們公司小程序收到線上反饋,在商品列表頁面為什么我劃著劃著劃著,就會出現(xiàn)一些重復(fù)商品......

圖片

在講這個問題之前,先講一下我們是如何實(shí)現(xiàn)瀑布流組件的

瀑布流組件

什么是瀑布流組件

如圖所示下方商品列表就采用了瀑布流的布局,視覺表現(xiàn)為參差不齊的多欄布局。

圖片

如何實(shí)現(xiàn)一個瀑布流組件

下面簡單寫一下實(shí)現(xiàn)瀑布流的思路,左右兩列布局,根據(jù)每一列的高度來判斷下次插入到哪一列中,每次插入列中需重新計算高度,將下一個節(jié)點(diǎn)插入短的哪一列中,如下圖所示:

圖片

圖片

下面代碼示例(僅展示思路)

// dataList 就是我們整個的商品卡片列表的數(shù)據(jù) ,用戶滑動到底部會加載新一頁的數(shù)據(jù) 會再次觸發(fā) 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')
  // 取下一頁數(shù)據(jù)
  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>

當(dāng)用戶滾動到底部的時候會加載下一頁的數(shù)據(jù),dataList 會發(fā)生變化,組件會監(jiān)聽到 dataList 的變化來執(zhí)行 dataRender,dataRender 中會去計算左右兩列的高度,哪邊更短來插入哪邊,循環(huán) list 來完成整個列表的插入。

商品重復(fù)的原因

乍一看上面代碼寫的很完美,但是卻忽略 DOM 渲染還需要時間,代碼中使用了 for await 保證異步循環(huán)有序進(jìn)行,并且保證數(shù)據(jù)變化后 DOM 能渲染完成后獲取到新的列高,這樣卻導(dǎo)致了 DOM 渲染的比較慢。DOM 在沒有加載完成的情況下,用戶再次滑動到底部會再次加載新的一頁數(shù)據(jù),導(dǎo)致 watch 又會被觸發(fā),dataRender 會再次被執(zhí)行,相當(dāng)于會存在多個 dataRender 同時在執(zhí)行。但是 dataRender 中使用到了全局的 leftDataList、rightDataList 和 lastIndex ,如果多個 dataRender 同時執(zhí)行的話就會到數(shù)據(jù)錯亂,lastIndex 錯亂會導(dǎo)致商品重復(fù),leftDataList 和 rightDataList 錯亂會導(dǎo)致順序問題。

下面用偽代碼講述一下之間的關(guān)系

// 正常情況代碼會像如下情況去走
list = [1,2,3,4,5]
// 數(shù)組執(zhí)行完成后
lastIndex = 5
// 加載下一頁數(shù)據(jù)后 
list = [1,2,3,4,5,6,7,8,9,10]

list.slice(lastIndex, list.length) // [6,7,8,9,10]

但是如果 dataRender 同時執(zhí)行 大家都共用同一個 lastIndex ,lastIndex 并不是最新的,就會變成下面這種情況

list.slice(lastIndex, list.length) // [1,2,3,4,5,6,7,8,9,10]

同理順序錯亂也是這種情況

解決方案

出現(xiàn)這個問題的原因是存在多個 dataRender 同時執(zhí)行,那我們只需想辦法在同一時間只能有一個在執(zhí)行就可以了。

方法一(復(fù)雜,不推薦):標(biāo)記位大法

看著這個方法相信大部分人經(jīng)常把它用作防抖節(jié)流,例如不想讓某個按鈕頻繁點(diǎn)擊導(dǎo)致發(fā)送過多的請求、點(diǎn)擊的時候讓某個請求完全返回結(jié)果后才能再次觸發(fā)下次請求等。因此我們這里的思路也是控制異步任務(wù)的次數(shù),在一個 dataRender 完全執(zhí)行完成之后才能執(zhí)行另一個 dataRender ,在這里我們首先添加一個全局標(biāo)記 fallLoad, 在最后一個節(jié)點(diǎn)渲染完才可以執(zhí)行 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')
    // 判斷是最后一個節(jié)點(diǎn)
    if((tempList.length - 1) === i) {
      fallLoad.value = true
    }
  }
  lastIndex.value = newList.length
}

這樣的話會丟棄掉用戶快速滑動時觸發(fā)的 dataRender ,只有在 DOM 渲染完成后再次觸發(fā)新的請求時才會再次觸發(fā)。但是這樣可能會存在另外一個問題,有部分的 dataRender 被丟棄掉了,同時用戶把所有的數(shù)據(jù)都加載完成了,沒有新的數(shù)據(jù)來觸發(fā) watch ,這就導(dǎo)致部分商品的數(shù)據(jù)準(zhǔn)備好了但在頁面上沒有渲染,因此我們還需要針對這種情況再去做單獨(dú)處理, ,我們可以額外加一個狀態(tài)來判斷 rightDataList + leftDataList 的總數(shù)是否等于 dataList,不等于的時候可以再觸發(fā)一次 dataRender ......

其實(shí)我們這種場景其實(shí)已經(jīng)不太適合用標(biāo)記位大法,強(qiáng)行使用只會讓代碼變成一座“屎山”,但是其實(shí)在我們?nèi)粘I(yè)務(wù)中,添加標(biāo)記位是一種很實(shí)用的方法,比如給某個按鈕添加 loading ,防止某些事件、請求頻繁執(zhí)行等。

方法二(優(yōu)雅,推薦):Promise + 隊(duì)列 大法

由于我們并不能丟棄異常情況觸發(fā)的 dataRender, 那我們只能讓 dataRender 有序的執(zhí)行。

我們重新整理思路,首先我們先把復(fù)雜的問題簡單化。拋開我們的業(yè)務(wù)場景,dataRender 就可以當(dāng)做一個異步的請求,然后問題就變成了在同一時間我們收到了多個異步的請求,我們怎么讓這些異步請求自動、有序執(zhí)行。

經(jīng)過上面的推導(dǎo)我們拆解出以下幾個關(guān)鍵點(diǎn):

  1. 我們需要一個隊(duì)列,隊(duì)列中存儲每個異步任務(wù)
  2. 當(dāng)把這個任務(wù)添加到這個隊(duì)列中的時候自動執(zhí)行第一個任務(wù)
  3. 我們需要使用 promise.then() 來保證任務(wù)有序的執(zhí)行
  4. 當(dāng)存隊(duì)列中在多個異步任務(wù)的時候,怎么在執(zhí)行完成第一個之后再去自動的執(zhí)行后續(xù)的任務(wù)

第一次執(zhí)行的時機(jī)其實(shí)我們是知道,那我們需要現(xiàn)在解決的問題是執(zhí)行完成第一個后怎么去自動執(zhí)行后續(xù)的請求?

圖片

  1. 使用循環(huán),可參考瀑布流組件中的 for await of 確保每次異步任務(wù)的執(zhí)行,這里就不過多闡述了,這么寫代碼不太優(yōu)雅
  2. 使用遞歸,在每個 promise.then 中遞歸下一個 promise

通過這幾點(diǎn)關(guān)鍵點(diǎn)我們寫出使用遞歸的方案的代碼:

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

每次調(diào)用 add 方法會往隊(duì)列中添加經(jīng)過特殊包裝過的異步任務(wù),并且只有在只有在沒有正在執(zhí)行中的任務(wù)的時候才開始執(zhí)行 execute 方法。在每次執(zhí)行異步任務(wù)時會從隊(duì)列中 shift ,利用 promise.then 并且遞歸調(diào)用該方法,實(shí)現(xiàn)有序并且自動執(zhí)行任務(wù)。在封裝在這方法的過程中同樣也使用到了我們的標(biāo)記位大法 inProgress ,來保證我們正在執(zhí)行當(dāng)前隊(duì)列時,突然又進(jìn)來新的任務(wù)而導(dǎo)致隊(duì)列執(zhí)行錯亂。

調(diào)用方法如下:

const queue = new asyncQueue()

watch(() => props.dataList, async (newVal, oldVal) => {
  queue.add(() => dataRender(newVal))
}, {
  immediate: true,
  deep: true
})

通過上述代碼我們就可以,讓我們的每一個異步任務(wù)有順序的執(zhí)行,并且讓每一個異步任務(wù)執(zhí)行完成以后自動執(zhí)行下一個,完美的達(dá)到了我的需求。

其實(shí)這個方法不僅適用于當(dāng)前場景,我們很多的業(yè)務(wù)場景都會遇到這種情況,會被動接受多個請求,但是這些請求還要有序的執(zhí)行,我們都可以使用這種方法。

下面我簡單列舉了兩種其他的場景:

  1. 比如某個按鈕用戶點(diǎn)擊了多次,但是我們要讓這些請求有序的執(zhí)行并且依次拿到這些請求返回的數(shù)據(jù)
  2. 某些高頻的通信操作,我們不能丟棄用戶的每次通信,而是需要用這種隊(duì)列的方式,自動、有序的執(zhí)行

總結(jié)

上述的這些“點(diǎn)” ,標(biāo)記位、promise、隊(duì)列、遞歸等,在日常開發(fā)中幾乎充斥在我們項(xiàng)目的每一個角落,但是如何使用好這些”點(diǎn)“值得我們深思的。

責(zé)任編輯:武曉燕 來源: 政采云技術(shù)
相關(guān)推薦

2010-12-14 11:42:45

職場

2022-04-14 15:53:12

開發(fā)瀑布流組件

2018-04-03 10:24:13

2017-05-02 13:38:51

CSS繪制形狀

2017-04-11 17:22:57

編程程序員語言

2021-03-22 11:10:09

Redis架構(gòu)MQ

2015-04-14 09:31:10

AWSAWS PaaSSaaS可視化編排

2018-08-10 14:57:03

UnixMySQL命令

2024-09-03 17:04:15

前端算法布局

2015-02-26 18:09:29

WaterFall V瀑布流Dynamic Gri

2017-05-02 20:56:36

機(jī)器學(xué)習(xí)HR簡歷

2022-02-20 19:02:16

RollupVue 2JavaScrip

2012-05-02 13:53:00

JavaScript

2020-01-13 14:39:06

FlinkSQL無限流

2024-08-19 14:01:00

2023-03-21 17:06:24

樹莓派路由器

2013-07-01 14:41:46

失敗移動APP移動創(chuàng)業(yè)

2020-07-03 14:18:51

運(yùn)營商流量用戶

2021-11-29 22:39:39

引擎Flink架構(gòu)

2020-04-21 08:30:32

AI人工智能語言
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號