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

從原理到實踐:如何在Taro中構(gòu)建高效且易用的虛擬列表

開發(fā) 前端
在通過scrollToIndex找到該節(jié)點即將渲染在第幾頁,在這之前的幾頁都需要我們手動執(zhí)行以下監(jiān)聽每一屏是否在可視區(qū),因為在這之后的頁都會通過觸底這一操作來執(zhí)行監(jiān)聽。

前言

最近在小程序的工作中有許多場景是大數(shù)據(jù)量的列表渲染,這種渲染場景如果不對它進(jìn)行優(yōu)化會非常耗性能,常見的優(yōu)化手段有:分片渲染與虛擬列表,恰好Taro官方也有提供虛擬列表組件,但有同事反饋這個組件并不好用,體驗也不好,白屏非常明顯。

在虛擬列表中滾動過快造成的白屏其實是必然的,關(guān)鍵在于我們怎么去優(yōu)化它,把白屏比例盡量降到最低,這其中還得權(quán)衡性能與體驗,想要白屏越少,那么你要渲染的節(jié)點就越多,性能自然也就越差。反之,你想要性能好,那么白屏就會增加。

接下來,自己動手實現(xiàn)一個虛擬列表,支持以下功能:

  • 元素定高,可支持滾動到指定元素位置
  • 元素不定高,無需關(guān)心子元素的高度,組件內(nèi)自動計算
  • 可支持一次性加載所有數(shù)據(jù),也支持分頁加載數(shù)據(jù)

原理介紹

虛擬列表的原理其實就是:只渲染可視區(qū)內(nèi)的元素,對于非可視區(qū)的元素不進(jìn)行渲染,這樣就可以提高長列表的渲染性能。

但是為了在滾動過程盡可能的降低白屏率,我們可以多渲染幾屏元素

圖片圖片

如上圖,原理大概是這樣:

  • 將數(shù)據(jù)處理成每一屏一個渲染單位,用二維數(shù)組存儲
  • 使用Taro.createIntersectionObserver監(jiān)聽每一屏內(nèi)容是否在可視區(qū),在可視區(qū)直接渲染,不在可視區(qū)則使用該屏真實高度進(jìn)行占位。高度怎么來?可以定高,也可以不定高,不定高的話就是等每一屏渲染完成后記錄渲染高度即可,兩種都有實現(xiàn),具體可以看下面的實現(xiàn)方法

這樣的話,每次真實渲染的節(jié)點數(shù)就遠(yuǎn)小于列表全部渲染的節(jié)點數(shù),可以極大地提高頁面渲染性能。

實現(xiàn)

處理數(shù)據(jù)

首先將外部的數(shù)據(jù)處理成二維數(shù)組,方便后續(xù)按屏渲染

// 處理列表數(shù)據(jù),按規(guī)則分割
let initList: any[] = []; // 初始列表(備用)
const dealList = (list: any[]) => {
  const segmentNum = props?.segmentNum; // 每頁顯示數(shù)量
  let arr: any[] = [];
  const _list: any[] = [];
  list.forEach((item, index) => {
    arr.push(item);
    if ((index + 1) % segmentNum === 0) {
      _list.push(arr);
      arr = [];
    }
  });
  // 處理余數(shù)
  const restList = list.slice(_list.length * segmentNum);
  if (restList.length) {
    _list.push(restList);
  }
  initList = _list;
};

計算渲染高度

在處理完數(shù)據(jù)后,接著我們就是取出每一屏的數(shù)據(jù)進(jìn)行渲染,接著計算并存儲渲染后每一屏所占用的高度,在后續(xù)滾動過程中,如果該屏內(nèi)容離開可視區(qū),我們就可以將該屏的內(nèi)容替換成對應(yīng)高度進(jìn)行渲染占位,這樣就可以減少真實渲染的節(jié)點數(shù)。

// 計算每一頁數(shù)據(jù)渲染完成后所占的高度
const setheight = (list: any[], pageIndex?: number) => {
  const index = pageIndex ?? renderPageIndex.value;
  const query = Taro.createSelectorQuery();

  query.select(`.inner_list_${index}`).boundingClientRect();
  query.exec((res) => {
    if (list?.length) {
      pageHeightArr.value.push(res?.[0]?.height);  // 存儲每一屏真實渲染高度
    }
  });
  observePageHeight(pageIndex); // 監(jiān)聽頁面高度
};

監(jiān)聽每一屏是否在可視區(qū)

上面提到的當(dāng)每一屏的內(nèi)容離開可視區(qū)就需要將該屏內(nèi)容替換為占位高度,這個功能的實現(xiàn)就需要借助Taro.createIntersectionObserver這個API來完成。

這里需要注意的是relativeToViewport可以自定義監(jiān)視區(qū)域,如果想要滾動過程減少白屏概率,那么可以將監(jiān)視區(qū)域擴(kuò)大,但渲染性能也會隨之變差,所有這里可以按自己的業(yè)務(wù)需要考量

const observePageHeight = (pageIndex?: number) => {
  const index = pageIndex ?? renderPageIndex.value;
  observer = Taro.createIntersectionObserver(
    currentPage.page as any,
  ).relativeToViewport({
    top: props?.screenNum * pageHeight,
    bottom: props?.screenNum * pageHeight,
  });
  console.log('observer', observer);
  // console.log("index", `.inner_list_${index}`);
  observer?.observe(`.inner_list_${index}`, (res) => {
    console.log(`.inner_list_${index}`, res.intersectionRatio);
    if (res.intersectionRatio <= 0) {
      // 當(dāng)沒有交集時,說明當(dāng)前頁面已經(jīng)不在視口內(nèi),則將該屏數(shù)據(jù)修改為該屏高度進(jìn)行占位
      towList.value[index] = {
        height: pageHeightArr.value[index],
      };
    } else {
      // 當(dāng)有交集時,說明當(dāng)前頁面在視口內(nèi)
      if (!towList.value[index]?.length) {
        towList.value[index] = initList[index];
      }
    }
  });
};

觸底監(jiān)聽

UI層,使用了scrollView組件來渲染列表,真實列表項渲染提供插槽給外部自行處理

<scroll-view
      v-if="list?.length"
      class="list"
      :scrollY="true"
      :showScrollbar="false"
      :lowerThreshold="lowerThreshold"
      :scrollTop="scrollTop"
      @scrollToLower="renderNext"
      :enhanced="true"
      :bounces="false"
      :enablePassive="true"
      :style="{ height: height }"
    >
      <view
        :class="[`inner_list_${pageIndex}`]"
        :id="`inner_list_${pageIndex}`"
        v-for="(page, pageIndex) in towList"
        :key="pageIndex"
      >
        <template v-if="page?.length > 0">
          <view
            :id="`item_${pageIndex}_${index}`"
            v-for="(item, index) in page"
            :key="index"
          >
            <slot v-if="item" name="listItem" :item="item"></slot>
          </view>
        </template>
        <view v-else :style="{ height: `${pageHeightArr[pageIndex]}px` }">
        </view>
      </view>
      <!-- 底部自定義內(nèi)容 -->
      <slot name="renderBottom"></slot>
    </scroll-view>

通過lowerThreshold監(jiān)聽觸底操作,將二維數(shù)組每一項取出來渲染,當(dāng)每一頁的內(nèi)容都渲染完后,那么頁面最終的所有節(jié)點將會是:真實列表內(nèi)容 + 占位高度,后續(xù)只需要依賴上一步驟的監(jiān)聽就可以完成真實內(nèi)容渲染與占位高度之間的切換。

// 渲染下一頁
const renderNext = () => {
  // if (!towList.value[pageIndex]?.length) {
  //   // 無數(shù)據(jù)
  // }
    renderPageIndex.value += 1; // 更新當(dāng)前頁索引
    if (renderPageIndex.value >= initList.length) {
      // 已經(jīng)到底
      return;
    }
    towList.value[renderPageIndex.value] = initList[renderPageIndex.value];

    Taro.nextTick(() => {
      setheight(props?.list);
    });
};

這樣基本就完成一個虛擬列表組件,我們來看看效果:

// 渲染數(shù)據(jù)
const list = ref(
  new Array(10000).fill(0).map((_, i) => {
    return {
      label: `第 ${i} 章`,
      value: i,
      isLock: false,
      time: "2023-01-12 16:07",
      type: "chapter",
    };
  }),
); // 列表數(shù)據(jù)

這里模擬了10000條數(shù)據(jù)來測試:

圖片圖片

初始渲染只有兩頁內(nèi)容,每一頁渲染20條。

當(dāng)我們滾動頁面時,就會根據(jù)監(jiān)聽來加入渲染內(nèi)容,并且將不在可視區(qū)的內(nèi)容替換成占位高度。

圖片圖片

但是我們的業(yè)務(wù)還需要定位功能,定位到某一章高亮,這里就需要計算滾動高度了,雖然scrollView組件提供了scrollIntoView屬性,可以使列表滾動到對應(yīng)子元素位置,但是我發(fā)現(xiàn)只有它的第一層子元素能夠生效,對于他的孫子元素并不生效。

定高滾動至指定位置

由于我這里是按頁來渲染的,需要定位到的元素并不是它的第一層子元素,所以這個方法在這里并不適用,最終只能計算滾動高度來實現(xiàn)。

const formateList = (list: any[]): void => {
  const scrollToIndex = props?.scrollToIndex; // 滾動到指定位置
  const itemHeight =
    itemRenderHeight.value || (props?.itemHeight ?? 0) * (pageWidth / 375); // 每一項的真實渲染高度
  const segmentNum = props?.segmentNum; // 每頁顯示數(shù)量
  dealList(list);
  if (itemHeight && scrollToIndex !== undefined) {
    // 定高,可滾動至指定位置
    // console.log("scrollToIndex", scrollToIndex);
    const startIndex = Math.floor(scrollToIndex / segmentNum); // 找到當(dāng)前索引所在的頁面
    console.log("startIndex", startIndex);
    renderPageIndex.value = startIndex; // 更新當(dāng)前頁索引
    const pageHeight = segmentNum * itemHeight; // 一屏的高度
    console.log("pageHeight", pageHeight, itemHeight);
    // readyList
    for (let i = 0; i < startIndex; i++) {
      pageHeightArr.value[i] = pageHeight;
      towList.value[i] = {
        height: pageHeight,
      };
    }
    towList.value[startIndex] = initList[startIndex];
    if (startIndex + 1 < initList.length) {
      towList.value[startIndex + 1] = initList[startIndex + 1];
    }
    Taro.nextTick(() => {
      for (let i = 0; i < startIndex; i++) {
        // observePageHeight(i);
        setheight(list, i);
      }
      scrollTop.value = scrollToIndex * itemHeight;
      console.log("scrollTop---", scrollTop.value);
    });
  } else {
    // console.log("當(dāng)前為不定高虛擬列表");
    towList.value = initList.slice(0, 1);
    Taro.nextTick(() => {
      setheight(list);
    });
  }
};

通過scrollToIndex計算出需要定位到的位置。

圖片圖片

這里需要注意的是,在通過scrollToIndex找到該節(jié)點即將渲染在第幾頁,在這之前的幾頁都需要我們手動執(zhí)行以下監(jiān)聽每一屏是否在可視區(qū),因為在這之后的頁都會通過觸底這一操作來執(zhí)行監(jiān)聽。如果少了這一步那么之前的這幾頁都會白屏,無真實數(shù)據(jù)渲染。

責(zé)任編輯:武曉燕 來源: 前端南玖
相關(guān)推薦

2024-07-07 21:49:22

2017-08-10 09:11:38

規(guī)則引擎構(gòu)建

2025-03-17 01:55:00

TCP服務(wù)迭代

2024-03-27 10:14:48

2021-05-11 07:51:30

React ref 前端

2023-12-13 13:15:13

平臺開發(fā)實踐

2025-04-01 01:04:00

Redis集群緩存

2010-06-29 14:20:52

2023-08-03 08:03:05

2018-05-17 15:18:48

Logistic回歸算法機(jī)器學(xué)習(xí)

2025-04-02 07:29:14

2024-12-17 08:04:04

2024-12-12 09:00:28

2024-01-19 16:35:23

2025-02-06 09:43:08

HybridFlowRay大語言模型

2023-07-10 08:26:19

2025-04-25 09:00:00

Transforme模型代碼

2025-04-03 00:03:00

數(shù)據(jù)內(nèi)存網(wǎng)絡(luò)

2022-02-28 10:05:12

組件化架構(gòu)設(shè)計從原組件化模塊化

2020-04-28 22:12:30

Nginx正向代理反向代理
點贊
收藏

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