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

面試官:不會“不定高”虛擬列表,你在簡歷上面提他干嘛?

開發(fā) 前端
這篇文章我們講了不定高的虛擬列表如何實現(xiàn),首先給每個item設置一個預估高度itemSize?。然后根據(jù)傳入的長列表數(shù)據(jù)listData?初始化一個positions?數(shù)組,數(shù)組中的top、bottom、height?等屬性表示每個item的位置。

前言

很多同學將虛擬列表當做亮點寫在簡歷上面,但是卻不知道如何手寫,那么這個就不是加分項而是減分項了。

什么是不定高虛擬列表

不定高的意思很簡單,就是不知道每一項item的具體高度,如下圖:

圖片圖片

現(xiàn)在我們有個問題,在不定高的情況下我們就不能根據(jù)當前滾動條的scrollTop去計算可視區(qū)域里面實際渲染的第一個item的index位置,也就是start的值。

沒有start,那么就無法實現(xiàn)在滾動的時候只渲染可視區(qū)域的那幾個item了。

預估高度

既然我們不知道每個item的高度,那么就采用預估高度的方式去實現(xiàn)。比如這樣:

const { listData, itemSize } = defineProps({
// 列表數(shù)據(jù)
listData: {
    type: Array,
    default: () => [],
  },
// 預估item高度,不是真實item高度
itemSize: {
    type: Number,
    default: 300,
  },
});

還是和上一篇一樣的套路,計算出當前可視區(qū)域的高度containerHeight,然后結合預估的itemSize就可以得到當前可視區(qū)域里面渲染的item數(shù)量。代碼如下:

const renderCount = computed(() => Math.ceil(containerHeight.value / itemSize));

注意:由于我們是預估的高度,所以這個renderCount的數(shù)量是不準的。

如果預估的高度比實際高太多,那么實際渲染的item數(shù)量就會不夠,導致頁面下方出現(xiàn)白屏的情況。

如果預估的高度太小,那么這里的item數(shù)量就會渲染的太多了,性能又沒之前那么好。

所以預估item高度需要根據(jù)實際業(yè)務去給一個適當?shù)闹?,理論上是寧可預估小點,也不預估的大了(大了會出現(xiàn)白屏)。

start初始值為0,并且算出了renderCount,此時我們也就知道了可視區(qū)域渲染的最后一個end的值。如下:

const end = computed(() => start.value + renderCount.value);

和上一篇一樣計算end時在下方多渲染了一個item,第一個item有一部分滾出可視區(qū)域的情況時,如果不多渲染可能就會出現(xiàn)白屏的情況。

有了start和end,那么就知道了可視區(qū)域渲染的renderList,代碼如下:

const renderList = computed(() => listData.slice(start.value, end.value + 1));

這樣我們就知道了,初始化時可視區(qū)域應該渲染哪些item了,但是因為我們之前是給每個item預估高度,所以我們應該將這些高度的值糾正過來。

更新高度

為了記錄不定高的list里面的每個item的高度,所以我們需要一個數(shù)組來存每個item的高度。所以我們需要定義一個positions數(shù)組來存這些值。

既然都存了每個item的高度,那么同樣可以使用top、bottom這兩個字段去記錄每個item在列表中的開始位置和結束位置。注意bottom - top的值肯定等于height的值。

還有一個index字段記錄每個item的index的值。positions定義如下:

const positions = ref<
  {
    index: number;
    height: number;
    top: number;
    bottom: number;
  }[]
>([]);

positions的初始化值為空數(shù)組,那么什么時候給這個數(shù)組賦值呢?

答案很簡單,虛擬列表渲染的是props傳入進來的listData。所以我們watch監(jiān)聽listData,加上immediate: true。這樣就可以實現(xiàn)初始化時給positions賦值,代碼如下:

watch(() => listData, initPosition, {
immediate: true,
});

function initPosition() {
  positions.value = [];
  listData.forEach((_item, index) => {
    positions.value.push({
      index,
      height: itemSize,
      top: index * itemSize,
      bottom: (index + 1) * itemSize,
    });
  });
}

遍歷listData結合預估的itemSize,我們就可以得出每一個item里面的height、top、bottom這幾個字段的值。

還有一個問題,我們需要一個元素來撐開滾動條。在定高的虛擬列表中我們是通過itemSize * listData.length得到的。顯然這里不能那樣做了,由于positions數(shù)組中存的是所有item的位置,那么最后一個item的bottom的值就是列表的真實高度。前面也是不準的,會隨著我們糾正positions中的值后他就是越來越準的了。

所以列表的真實高度為:

const listHeight = computed(
  () => positions.value[positions.value.length - 1].bottom
);

此時positions數(shù)組中就已經(jīng)記錄了每個item的具體位置,雖然這個位置是錯的。接下來我們就需要將這些錯誤的值糾正過來,如何糾正呢?

答案很簡單,使用Vue的onUpdated鉤子函數(shù),這個鉤子函數(shù)會在響應式狀態(tài)變更而更新其 DOM 樹之后調(diào)用。也就是會在renderList渲染成DOM后觸發(fā)!

此時這些item已經(jīng)渲染成了DOM節(jié)點,那么我們就可以遍歷這些item的DOM節(jié)點拿到每個item的真實高度。都知道每個item的真實高度了,那么也就能夠更新里面所有item的top和bottom了。代碼如下:

<template>
  <div ref="container" class="container" @scroll="handleScroll($event)">
    <div class="placeholder" :style="{ height: listHeight + 'px' }"></div>
    <div class="list-wrapper" :style="{ transform: getTransform }">
      <div
        class="card-item"
        v-for="item in renderList"
        :key="item.index"
        ref="itemRefs"
        :data-index="item.index"
      >
        <span style="color: red"
          >{{ item.index }}
          <img width="200" :src="item.imgUrl" alt="" />
        </span>
        {{ item.value }}
      </div>
    </div>
  </div>
</template>

<script setup>
onUpdated(() => {
  updatePosition();
});

function updatePosition() {
  itemRefs.value.forEach((el) => {
    const index = +el.getAttribute("data-index");
    const realHeight = el.getBoundingClientRect().height;
    let diffVal = positions.value[index].height - realHeight;
    const curItem = positions.value[index];
    if (diffVal !== 0) {
      // 說明item的高度不等于預估值
      curItem.height = realHeight;
      curItem.bottom = curItem.bottom - diffVal;
      for (let i = index + 1; i < positions.value.length - 1; i++) {
        positions.value[i].top = positions.value[i].top - diffVal;
        positions.value[i].bottom = positions.value[i].bottom - diffVal;
      }
    }
  });
}
</script>

使用:data-index="item.index"將index綁定到item上面,更新時就可以通過+el.getAttribute("data-index")拿到對應item的index。

itemRefs中存的是所有item的DOM元素,遍歷他就可以拿到每一個item,然后拿到每個item在長列表中的index和真實高度realHeight。

diffVal的值是預估的高度比實際的高度大多少,如果diffVal的值不等于0,說明預估的高度不準。此時就需要將當前item的高度height更新了,由于高度只會影響bottom的值,所以只需要更新當前item的height和bottom。

由于當前item的高度變了,假如diffVal的值為正值,說明我們預估的高度多了。此時我們需要從當前item的下一個元素開始遍歷,直到遍歷完整個長列表。我們預估多了,那么只需要將后面的所有item整體都向上移一移,移動的距離就是預估的差值diffVal。

所以這里需要從index + 1開始遍歷,將遍歷到的所有元素的top和bottom的值都減去diffVal。

將可視區(qū)域渲染的所有item都遍歷一遍,將每個item的高度和位置都糾正過來,同時會將后面沒有渲染到的item的top和bottom都糾正過來,這樣就實現(xiàn)了高度的更新。理論上從頭滾到尾,那么整個長列表里面的所有位置和高度都糾正完了。

開始滾動

通過前面我們已經(jīng)實現(xiàn)了預估高度值的糾正,渲染過的item的高度和位置都是糾正過后的了。此時我們需要在滾動后如何計算出新的start的位置,以及offset偏移量的值。

還是和定高同樣的套路,當滾動條在item中間滾動時復用瀏覽器的滾動條,從一個item滾到另外一個item時才需要更新start的值以及offset偏移量的值。如果你看不懂這句話,建議先看我上一篇如何實現(xiàn)一個定高虛擬列表 文章。

此時應該如何計算最新的start值呢?

很簡單!在positions中存了兩個字段分別是top和bottom,分別表示當前item的開始位置和結束位置。如果當前滾動條的scrollTop剛好在top和bottom之間,也就是scrollTop >= top && scrollTop < bottom,那么是不是就說明當前剛好滾到這個item的位置呢。

并且由于在positions數(shù)組中bottom的值是遞增的,那么問題不就變成了查找第一個item的scrollTop < bottom。所以我們得出:

function getStart(scrollTop) {
  return positions.value.findIndex((item) => scrollTop < item.bottom);
}

每次scroll滾動都會觸發(fā)一次這個查找,那么我們可以優(yōu)化上面的算法嗎?

positions數(shù)組中的bottom字段是遞增的,這很符合二分查找的規(guī)律。不了解二分查找的同學可以看看leetcode上面的這道題: https://leetcode.cn/problems/search-insert-position/description/。

所以上面的代碼可以優(yōu)化成這樣:

function getStart(scrollTop) {
let left = 0;
let right = positions.value.length - 1;
while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (positions.value[mid].bottom === scrollTop) {
      return mid + 1;
    } elseif (positions.value[mid].bottom < scrollTop) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
return left;
}

和定高的虛擬列表一樣,當在start的item中滾動時直接復用瀏覽器的滾動,無需做任何事情。所以此時的offset偏移量就應該等于當前start的item的top值,也就是start的item前面的所有item加起來的高度。所以得出offset的值為:

offset.value = positions.value[start.value].top;

可能有的小伙伴會迷惑,在start的item中的滾動值為什么不算到offset偏移中去呢?

因為在start的item范圍內(nèi)滾動時都是直接使用的瀏覽器滾動,已經(jīng)有了scrollTop,所以無需加到offset偏移中去。

所以我們得出當scroll事件觸發(fā)時代碼如下:

function handleScroll(e) {
  const scrollTop = e.target.scrollTop;
  start.value = getStart(scrollTop);
  offset.value = positions.value[start.value].top;
}

同樣offset偏移值使用translate3d應用到可視區(qū)域的div上面,代碼如下:

<template>
  <div ref="container" class="container" @scroll="handleScroll($event)">
    <div class="placeholder" :style="{ height: listHeight + 'px' }"></div>
    <div class="list-wrapper" :style="{ transform: getTransform }">
      ...省略
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  offset: {
    type: Number,
    default: 0,
  },
});
const getTransform = computed(() => `translate3d(0,${props.offset}px,0)`);
</script>

這個是最終的運行效果圖:

圖片圖片

完整的父組件代碼如下:

<template>
  <div style="height: 100vh; width: 100vw">
    <VirtualList :listData="data" :itemSize="50" />
  </div>
</template>

<script setup>
import VirtualList from "./dynamic.vue";
import { faker } from "@faker-js/faker";
import { ref } from "vue";

const data = ref([]);
for (let i = 0; i < 1000; i++) {
  data.value.push({
    index: i,
    value: faker.lorem.sentences(),
  });
}
</script>

<style>
html {
  height: 100%;
}
body {
  height: 100%;
  margin: 0;
}
#app {
  height: 100%;
}
</style>

完整的虛擬列表子組件代碼如下:

<template>
  <div ref="container" class="container" @scroll="handleScroll($event)">
    <div class="placeholder" :style="{ height: listHeight + 'px' }"></div>
    <div class="list-wrapper" :style="{ transform: getTransform }">
      <div
        class="card-item"
        v-for="item in renderList"
        :key="item.index"
        ref="itemRefs"
        :data-index="item.index"
      >
        <span style="color: red"
          >{{ item.index }}
          <img width="200" :src="item.imgUrl" alt="" />
        </span>
        {{ item.value }}
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted, onUpdated } from "vue";
const { listData, itemSize } = defineProps({
  // 列表數(shù)據(jù)
  listData: {
    type: Array,
    default: () => [],
  },
  // 預估item高度,不是真實item高度
  itemSize: {
    type: Number,
    default: 300,
  },
});

const container = ref(null);
const containerHeight = ref(0);
const start = ref(0);
const offset = ref(0);
const itemRefs = ref();
const positions = ref<
  {
    index: number;
    height: number;
    top: number;
    bottom: number;
  }[]
>([]);

const end = computed(() => start.value + renderCount.value);
const renderList = computed(() => listData.slice(start.value, end.value + 1));
const renderCount = computed(() => Math.ceil(containerHeight.value / itemSize));
const listHeight = computed(
() => positions.value[positions.value.length - 1].bottom
);
const getTransform = computed(() =>`translate3d(0,${offset.value}px,0)`);

watch(() => listData, initPosition, {
immediate: true,
});

function handleScroll(e) {
const scrollTop = e.target.scrollTop;
  start.value = getStart(scrollTop);
  offset.value = positions.value[start.value].top;
}

function getStart(scrollTop) {
let left = 0;
let right = positions.value.length - 1;
while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (positions.value[mid].bottom === scrollTop) {
      return mid + 1;
    } elseif (positions.value[mid].bottom < scrollTop) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
return left;
}

function initPosition() {
  positions.value = [];
  listData.forEach((_item, index) => {
    positions.value.push({
      index,
      height: itemSize,
      top: index * itemSize,
      bottom: (index + 1) * itemSize,
    });
  });
}

function updatePosition() {
  itemRefs.value.forEach((el) => {
    const index = +el.getAttribute("data-index");
    const realHeight = el.getBoundingClientRect().height;
    let diffVal = positions.value[index].height - realHeight;
    const curItem = positions.value[index];
    if (diffVal !== 0) {
      // 說明item的高度不等于預估值
      curItem.height = realHeight;
      curItem.bottom = curItem.bottom - diffVal;
      for (let i = index + 1; i < positions.value.length - 1; i++) {
        positions.value[i].top = positions.value[i].top - diffVal;
        positions.value[i].bottom = positions.value[i].bottom - diffVal;
      }
    }
  });
}

onMounted(() => {
  containerHeight.value = container.value.clientHeight;
});

onUpdated(() => {
  updatePosition();
});
</script>

<style scoped>
.container {
  height: 100%;
  overflow: auto;
  position: relative;
}

.placeholder {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.card-item {
  padding: 10px;
  color: #777;
  box-sizing: border-box;
  border-bottom: 1px solid #e1e1e1;
}
</style>

總結

這篇文章我們講了不定高的虛擬列表如何實現(xiàn),首先給每個item設置一個預估高度itemSize。然后根據(jù)傳入的長列表數(shù)據(jù)listData初始化一個positions數(shù)組,數(shù)組中的top、bottom、height等屬性表示每個item的位置。然后根據(jù)可視區(qū)域的高度加上itemSize算出可視區(qū)域內(nèi)可以渲染多少renderCount個item。接著就是在onUpdated鉤子函數(shù)中根據(jù)每個item的實際高度去修正positions數(shù)組中的值。

在滾動時查找第一個item的bottom大于scrollTop,這個item就是start的值。offset偏移的值為start的top屬性。

值得一提的是如果不定高的列表中有圖片就不能在onUpdated鉤子函數(shù)中修正positions數(shù)組中的值,而是應該監(jiān)聽圖片加載完成后再去修正positions數(shù)組??梢允褂?nbsp;ResizeObserver 去監(jiān)聽渲染的這一堆item,注意ResizeObserver的回調(diào)會觸發(fā)兩次,第一次為渲染item的時候,第二次為item中的圖片加載完成后。

責任編輯:武曉燕 來源: 前端歐陽
相關推薦

2020-05-21 10:02:51

Explain SQL優(yōu)化

2025-02-21 15:25:54

虛擬線程輕量級

2015-08-13 10:29:12

面試面試官

2025-04-11 07:46:09

2025-03-10 11:48:22

項目服務設計

2020-06-22 08:16:16

哈希hashCodeequals

2024-02-28 10:14:47

Redis數(shù)據(jù)硬盤

2021-02-06 09:21:17

MySQL索引面試

2018-04-27 14:46:07

面試簡歷程序員

2024-04-02 09:45:27

線程池Executors開發(fā)

2020-11-06 07:11:40

內(nèi)存虛擬Redis

2023-10-28 09:13:32

系統(tǒng)面試官架構

2015-08-24 09:00:36

面試面試官

2021-11-25 10:18:42

RESTfulJava互聯(lián)網(wǎng)

2025-04-01 00:00:00

項目CRUD單例模式

2021-08-03 07:51:43

React項目面試

2021-08-09 07:47:40

Git面試版本

2025-01-13 09:24:32

2023-12-20 14:35:37

Java虛擬線程

2021-08-05 12:41:57

高并發(fā)性能CAS
點贊
收藏

51CTO技術棧公眾號