成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

前言

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

什么是不定高虛擬列表

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

圖片圖片

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

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

預估高度

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

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

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

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

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

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

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

所以預估item高度需要根據實際業務去給一個適當的值,理論上是寧可預估小點,也不預估的大了(大了會出現白屏)。

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

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

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

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

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

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

更新高度

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

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

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

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

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

答案很簡單,虛擬列表渲染的是props傳入進來的listData。所以我們watch監聽listData,加上immediate: true。這樣就可以實現初始化時給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數組中存的是所有item的位置,那么最后一個item的bottom的值就是列表的真實高度。前面也是不準的,會隨著我們糾正positions中的值后他就是越來越準的了。

所以列表的真實高度為:

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

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

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

此時這些item已經渲染成了DOM節點,那么我們就可以遍歷這些item的DOM節點拿到每個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。

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

開始滾動

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

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

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

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

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

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

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

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

所以上面的代碼可以優化成這樣:

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范圍內滾動時都是直接使用的瀏覽器滾動,已經有了scrollTop,所以無需加到offset偏移中去。

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

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

同樣offset偏移值使用translate3d應用到可視區域的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({
  // 列表數據
  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>

總結

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

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

值得一提的是如果不定高的列表中有圖片就不能在onUpdated鉤子函數中修正positions數組中的值,而是應該監聽圖片加載完成后再去修正positions數組。可以使用 ResizeObserver 去監聽渲染的這一堆item,注意ResizeObserver的回調會觸發兩次,第一次為渲染item的時候,第二次為item中的圖片加載完成后。

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

2020-05-21 10:02:51

Explain SQL優化

2015-08-13 10:29:12

面試面試官

2025-04-11 07:46:09

2025-02-21 15:25:54

虛擬線程輕量級

2025-03-10 11:48:22

項目服務設計

2020-06-22 08:16:16

哈希hashCodeequals

2024-02-28 10:14:47

Redis數據硬盤

2024-04-02 09:45:27

線程池Executors開發

2021-02-06 09:21:17

MySQL索引面試

2018-04-27 14:46:07

面試簡歷程序員

2020-11-06 07:11:40

內存虛擬Redis

2021-08-09 07:47:40

Git面試版本

2025-01-13 09:24:32

2015-08-24 09:00:36

面試面試官

2021-11-25 10:18:42

RESTfulJava互聯網

2025-04-01 00:00:00

項目CRUD單例模式

2021-08-03 07:51:43

React項目面試

2023-10-28 09:13:32

系統面試官架構

2021-08-05 12:41:57

高并發性能CAS

2023-12-20 14:35:37

Java虛擬線程
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 中文字幕乱码视频32 | 色婷婷综合久久久中字幕精品久久 | 国产不卡一| 久久婷婷国产麻豆91 | 国产一区二区三区四区三区四 | 在线不卡 | 天天干天天色 | 国产一区二区三区在线 | 精品欧美一区免费观看α√ | 国产精品日女人 | 91精品国产综合久久久久久蜜臀 | 国产露脸国语对白在线 | 成人av在线大片 | 国产草草视频 | 中文字幕免费中文 | 欧美黄视频 | 国产第二页 | 久久人体视频 | 亚洲一区二区在线免费观看 | 午夜小视频在线播放 | 欧美片网站免费 | 欧美不卡视频一区发布 | 成人精品久久久 | 久久亚洲国产精品 | 亚洲一区二区久久久 | 成人免费视频网站在线看 | 成人网在线 | 国产丝袜一区二区三区免费视频 | 国产一区二区三区在线看 | 国内精品视频一区二区三区 | 人人干在线视频 | 欧美综合久久 | 亚洲区一| 亚洲三区视频 | 久久久天堂 | 可以免费看的毛片 | 国产综合久久 | 日本一区二区高清不卡 | 365夜爽爽欧美性午夜免费视频 | 超碰在线观看97 | 特黄色一级毛片 |