欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

React虛擬列表的實(shí)現(xiàn)

 更新時(shí)間:2021年05月17日 11:42:28   作者:趙_葉紫  
在開發(fā)過程中,總是遇到很多列表的顯示。當(dāng)上數(shù)量級(jí)別的列表渲染于瀏覽器,終會(huì)導(dǎo)致瀏覽器的性能下降,你可以選擇其他方式避免,本文就介紹了虛擬列表來(lái)解決這個(gè)問題

1.背景

在開發(fā)過程中,總是遇到很多列表的顯示。當(dāng)上數(shù)量級(jí)別的列表渲染于瀏覽器,終會(huì)導(dǎo)致瀏覽器的性能下降。如果數(shù)據(jù)量過大,首先渲染極慢,其次頁(yè)面直接卡死。當(dāng)然,你可以選擇其他方式避免。例如分頁(yè),或者下載文件等等。我們這里討論如果使用虛擬列表來(lái)解決這個(gè)問題。

2.什么是虛擬列表

最簡(jiǎn)單的描述:列表滾動(dòng)時(shí),變更可視區(qū)域內(nèi)的渲染元素。

通過 [單條數(shù)據(jù)預(yù)估高度] 計(jì)算出 [列表總高度]和[可視化區(qū)域高度 ]。并在[可視化區(qū)域高度]內(nèi)按需渲染列表。

3.相關(guān)概念簡(jiǎn)介

下面介紹在組件中,很重要的一些參數(shù)信息,這里先進(jìn)行了解,有個(gè)印象,后續(xù)在使用的時(shí)候才比較明朗。

  • [單條數(shù)據(jù)預(yù)估高度]: 列表中具體某一條列表的具體高度,它可以是 [固定高度],也可以是[動(dòng)態(tài)高度]
  • [列表總高度]: 當(dāng)所有數(shù)據(jù)渲染時(shí),列表的[總高度]
  • [可視化區(qū)域高度]: 掛在虛擬列表的容器。即列表可見的區(qū)域
  • [預(yù)估顯示條數(shù)]: 在 [可視化區(qū)域高度] 按照 [單條數(shù)據(jù)預(yù)估高度],可見的數(shù)據(jù)條數(shù)
  • [開始索引]: [可視化區(qū)域高度] 顯示的數(shù)據(jù)的第一條數(shù)據(jù)的索引
  • [結(jié)束索引]: [可視化區(qū)域高度] 顯示的數(shù)據(jù)的最后一條數(shù)據(jù)的索引
  • [每條Item 位置緩存]: 因?yàn)榱斜淼母叨炔灰欢?,因此?huì)對(duì)每條數(shù)據(jù)的高度位置進(jìn)行記錄,包括 index索引,top, bottom, lineHeight屬性

4.虛擬列表實(shí)現(xiàn)

虛擬列表可以簡(jiǎn)單理解為:當(dāng)列表發(fā)生滾動(dòng)時(shí),變更[可視化區(qū)域高度 ]內(nèi)的渲染元素,根據(jù)上面介紹的相關(guān)概念,我們依據(jù)這些屬性,按照以下步驟進(jìn)行:

  • 傳入組件數(shù)據(jù) [數(shù)據(jù)列表(resources)] 和 [預(yù)估高度(estimatedItemSize]
  • 根據(jù) [數(shù)據(jù)列表(resources)]和 [預(yù)估高度(estimatedItemSize] 計(jì)算出每條數(shù)據(jù)的初始位置(當(dāng)全部渲染時(shí)每條數(shù)據(jù)的占位)
  • 計(jì)算出 [列表總高度]
  • [可視化區(qū)域高度] 通過css控制
  • 根據(jù) [可視化區(qū)域高度],計(jì)算出可視化區(qū)域預(yù)估顯示條數(shù)
  • 初始化可視窗口的 [頭掛載元素]和[尾掛載元素],當(dāng)發(fā)生滾動(dòng)時(shí),根據(jù)滾動(dòng)差值和滾動(dòng)方向,重新計(jì)算[頭掛載元素]和[尾掛載元素]。

依據(jù)以上的簡(jiǎn)介步驟,下面開始來(lái)實(shí)現(xiàn)一個(gè)虛擬列表吧。

4.1 驅(qū)動(dòng)開發(fā):參數(shù)剖析

參數(shù) 說明 類型 默認(rèn)值
resources 源數(shù)據(jù)數(shù)組 Array []
estimatedItemSize 每條數(shù)據(jù)的預(yù)估高度 number 32px
extrea 用于自定義ItemRender,傳遞其他參數(shù) any none
ItemRender 每一條數(shù)據(jù)渲染的組件 React.FC const ItemRender = ({ data }: Data) => (<React.Fragment>{String(data) }</React.Fragment>)
key 作為遍歷時(shí),生成item 的唯一key。需要是resources的數(shù)據(jù)具體的某個(gè)唯一值的字段。用于提高性能。 string 默認(rèn)順序 自定義 -> id -> key -> index

4.1.1 ItemRender

import React, { useState } from 'react';
import { VirtualList } from 'biz-web-library';
// 定義每一條數(shù)據(jù)顯示的組件
const ItemRender = ({ data }) => {
  let dindex = parseInt(data);
  let lineHeight = dindex % 2 ? '40px' : '80px';
  return (
    <div style={{ lineHeight, background: dindex % 2 ? '#f5f5f5' : '#fff' }}>
      <h3>#{dindex} title name</h3>
      <p>盡情地書寫你想編寫的內(nèi)容,不局限于頁(yè)面高度</p>
    </div>
  );
};
const ItemRenderMemo = React.memo(ItemRender);

4.1.2 數(shù)據(jù)列表初始化

// 初始化列表數(shù)據(jù)
const getDatas = () => {
  const datas = [];
  for (let i = 0; i < 100000; i++) {
    datas.push(`${i} Item`);
  }
  return datas;
};

4.1.3 如何使用

// 使用虛擬列表
export default () => {
  let [resources, setResources] = useState([]);
  const changeResources = () => {
    setResources(getDatas());
  };

  return (
    <div>
      <button onClick={changeResources}>click me </button>

      <div
        style={{
          height: '400px',
          overflow: 'auto',
          border: '1px solid #f5f5f5',
          padding: '0 10px',
        }}
      >
        <VirtualList
          ItemRender={ItemRenderMemo}
          resources={resources}
          estimatedItemSize={60}
        />
      </div>
    </div>
  );
};

4.2 組件初始化計(jì)算和布局

現(xiàn)在,如何使用已經(jīng)知道,那么開始實(shí)現(xiàn)我們的組件吧。根據(jù)傳入的數(shù)據(jù)源resources和預(yù)估高度estimatedItemSize,計(jì)算出每一條數(shù)據(jù)的初始化位置。

// 循環(huán)緩存列表的總體初始化高度
export const initPositinoCache = (
  estimatedItemSize: number = 32,
  length: number = 0,
) => {
  let index = 0,
  positions = Array(length);
  while (index < length) {
    positions[index] = {
      index,
      height: estimatedItemSize,
      top: index * estimatedItemSize,
      bottom: (index++ + 1) * estimatedItemSize,
    };
  }
  return positions;
};

如果列表每條數(shù)據(jù)的高度一致,那么這個(gè)高度確實(shí)是不會(huì)改變的。如果每一條數(shù)據(jù)的高度不固定,那么該位置會(huì)在滾動(dòng)的過程中進(jìn)行更新。下面統(tǒng)計(jì)一些其他需要初始化的參數(shù):

參數(shù) 說明 類型 默認(rèn)值
resources 源數(shù)據(jù)數(shù)組 Array []
startOffset 可視區(qū)域距離頂部的偏移量 number 0
listHeight 所有數(shù)據(jù)渲染時(shí),容器的高度 any none
visibleCount 一頁(yè)可視化區(qū)域條數(shù) number 10
startIndex 可視化區(qū)域開始索引 number 0
endIndex 可視化區(qū)域結(jié)束索引 number 10
visibleData 可視化區(qū)域顯示的數(shù)據(jù) Array []

其實(shí)對(duì)于每一個(gè)屬性,介紹一下就清楚它的意義所在。但是 [startOffset]這個(gè)參數(shù)需要重點(diǎn)介紹一下。它就是在滾動(dòng)過程中,模擬無(wú)限滾動(dòng)的重要屬性。它的值,表示我們滾動(dòng)過程中距離頂部的位置。[startOffset]通過結(jié)合[visibleData]達(dá)到了無(wú)限滾動(dòng)的效果。
tips: 這里注意 [positions]的位置,相當(dāng)于一個(gè)組件的外部變量。記得不要掛在到組件的static屬性上面。

// 緩存所有item的位置
let positions: Array<PositionType>;

class VirtualList extends React.PureComponent{
 
  constructor(props) {
    super(props);
    const { resources } = this.props;

    // 初始化緩存
    positions = initPositinoCache(props.estimatedItemSize, resources.length);
    this.state = {
      resources,
      startOffset: 0,
      listHeight: getListHeight(positions),  // positions最后一條數(shù)據(jù)的bottom屬性

      scrollRef: React.createRef(),  // 虛擬列表容器ref
      items: React.createRef(), // 虛擬列表顯示區(qū)域ref
      visibleCount: 10, // 一頁(yè)可視區(qū)域條數(shù)
      startIndex: 0, // 可視區(qū)域開始索引
      endIndex: 10, // // 可視區(qū)域結(jié)束索引
    };
  }
  // TODO: 隱藏一些其他功能。。。。。


  // 布局
  render() {
  const { ItemRender = ItemRenderComponent, extrea } = this.props;
  const { listHeight, startOffset, resources, startIndex, endIndex, items, scrollRef  } = this.state;
  let visibleData = resources.slice(startIndex, endIndex);

  return (
    <div ref={scrollRef} style={{ height: `${listHeight}px` }}>
      <ul
        ref={items}
        style={{
          transform: `translate3d(0,${startOffset}px,0)`,
        }}
      >
        {visibleData.map((data, index) => {
          return (
            <li key={data.id || data.key || index} data-index={`${startIndex + index}`}>
              <ItemRender data={data} {...extrea}/>
            </li>
          );
        })}
      </ul>
    </div>
  );
  }
}

4.3 滾動(dòng)觸發(fā)注冊(cè)事件與更新

將onScroll通過[componentDidMount]注冊(cè)到dom上。滾動(dòng)事件中,使用的requestAnimationFrame,該方法是利用瀏覽器的空余時(shí)間進(jìn)行執(zhí)行,可以提高代碼的性能。大家想進(jìn)行深入理解,可以去查閱該api的具體使用。

componentDidMount() {
  events.on(this.getEl(), 'scroll', this.onScroll, false);
  events.on(this.getEl(), 'mousewheel', NOOP, false);

  // 根據(jù)渲染,計(jì)算最新的節(jié)點(diǎn)
  let visibleCount = Math.ceil(this.getEl().offsetHeight / estimatedItemSize);
  if (visibleCount === this.state.visibleCount || visibleCount === 0) {
    return;
  }
  // 因?yàn)?visibleCount變更, 更新endIndex, listHeight/ 偏移量
  this.updateState({ visibleCount, startIndex: this.state.startIndex });
}

getEl = () => {
    let el = this.state.scrollRef || this.state.items;
    let parentEl: any = el.current?.parentElement;
    switch (window.getComputedStyle(parentEl)?.overflowY) {
      case 'auto':
      case 'scroll':
      case 'overlay':
      case 'visible':
        return parentEl;
    }
    return document.body;
};

onScroll = () => {
    requestAnimationFrame(() => {
      let { scrollTop } = this.getEl();
      let startIndex = binarySearch(positions, scrollTop);

      // 因?yàn)?startIndex變更, 更新endIndex, listHeight/ 偏移量
      this.updateState({ visibleCount: this.state.visibleCount, startIndex});
    });
  };

接下來(lái)我們分析一下重點(diǎn)步驟。當(dāng)進(jìn)行滾動(dòng)時(shí),我們是可以拿到當(dāng)前[scrollRef]虛擬列表容器的 [scrollTop],通過該距離和[positions](記錄了每個(gè)item的所有位置屬性),可以拿到該位置的startIndex。這里為提高性能,我們通過二分法查找:

// 工具函數(shù),放入工具文件
export const binarySearch = (list: Array<PositionType>, value: number = 0) => {
  let start: number = 0;
  let end: number = list.length - 1;
  let tempIndex = null;
  while (start <= end) {
    let midIndex = Math.floor((start + end) / 2);
    let midValue = list[midIndex].bottom;

    // 值相等,則直接返回 查找到的節(jié)點(diǎn)(因?yàn)槭莃ottom, 因此startIndex應(yīng)該是下一個(gè)節(jié)點(diǎn))
    if (midValue === value) {
      return midIndex + 1;
    }
    // 中間值 小于 傳入值,則說明 value對(duì)應(yīng)的節(jié)點(diǎn) 大于 start, start往后移動(dòng)一位
    else if (midValue < value) {
      start = midIndex + 1;
    }
    // 中間值 大于 傳入值,則說明 value 在 中間值之前,end 節(jié)點(diǎn)移動(dòng)到 mid - 1
    else if (midValue > value) {
      // tempIndex存放最靠近值為value的所有
      if (tempIndex === null || tempIndex > midIndex) {
        tempIndex = midIndex;
      }
      end = midIndex - 1;
    }
  }
  return tempIndex;
};

獲取到startIndex,那么我們就依據(jù)startIndex來(lái)更新組件State中所有的屬性的值。

 updateState = ({ visibleCount, startIndex }) => {
    // 根據(jù)新計(jì)算的節(jié)點(diǎn),更新data數(shù)據(jù)
    this.setState({
      startOffset: startIndex >= 1 ? positions[startIndex - 1]?.bottom : 0,
      listHeight: getListHeight(positions),
      startIndex,
      visibleCount,
      endIndex: getEndIndex(this.state.resources, startIndex, visibleCount)
    });
  };

// 下面是工具函數(shù),放在其他文件中的
export const getListHeight = (positions: Array<PositionType>) => {
    let index = positions.length - 1;
    return index < 0 ? 0 : positions[index].bottom;
  };

export const getEndIndex = (
  resources: Array<Data>,
  startIndex: number,
  visibleCount: number,
) => {
  let resourcesLength = resources.length;
  let endIndex = startIndex + visibleCount;
  return resourcesLength > 0 ? Math.min(resourcesLength, endIndex) : endIndex;
}

4.4 item高度不等更新

至此,我們對(duì)于基本的dom進(jìn)行滾動(dòng),數(shù)據(jù)更新等邏輯完成。但是在測(cè)試過程中,會(huì)發(fā)現(xiàn),如果高度不等,還沒進(jìn)行更新position等操作呢?這些放在哪里呢?
這里,我們的[componentDidUpdate]就該派上用場(chǎng)了。每一次dom完成渲染,那么此時(shí)就應(yīng)該將顯示出來(lái)的item的 位置高度信息更新到 [position]屬性中。當(dāng)前 總高度[istHeight] 和偏移量[startOffset]也得同時(shí)進(jìn)行更新。

 componentDidUpdate() {
  this.updateHeight();
}

  
updateHeight = () => {
  let items: HTMLCollection = this.state.items.current?.children;
  if (!items.length) return;

  // 更新緩存
  updateItemSize(positions, items);

  // 更新總高度
  let listHeight = getListHeight(positions);

  // 更新總偏移量
  let startOffset = getStartOffset(this.state.startIndex, positions);

  this.setState({
    listHeight,
    startOffset,
  });
};

// 下面是工具函數(shù),放在其他文件中的
export const updateItemSize = (
  positions: Array<PositionType>,
  items: HTMLCollection,
) => {
  Array.from(items).forEach(item => {
    let index = Number(item.getAttribute('data-index'));
    let { height } = item.getBoundingClientRect();
    let oldHeight = positions[index].height;

    //存在差值, 更新該節(jié)點(diǎn)以后所有的節(jié)點(diǎn)
    let dValue = oldHeight - height;
    if (dValue) {
      positions[index].bottom = positions[index].bottom - dValue;
      positions[index].height = height;

      for (let k = index + 1; k < positions.length; k++) {
        positions[k].top = positions[k - 1].bottom;
        positions[k].bottom = positions[k].bottom - dValue;
      }
    }
  });
};

//獲取當(dāng)前的偏移量
export const getStartOffset = (
  startIndex: number,
  positions: Array<PositionType> = [],
) => {
  return startIndex >= 1 ? positions[startIndex - 1]?.bottom : 0;
};

export const getListHeight = (positions: Array<PositionType>) => {
  let index = positions.length - 1;
  return index < 0 ? 0 : positions[index].bottom;
};

4.5 外部參數(shù)數(shù)據(jù)變更,更新組件數(shù)據(jù)

當(dāng)前最后一步,如果我們傳入的外部數(shù)據(jù)源等進(jìn)行了變更,那么我們就得同步數(shù)據(jù)。該操作當(dāng)然是發(fā)放在 getDerivedStateFromProps方法完成。

 static getDerivedStateFromProps(
    nextProps: VirtualListProps,
    prevState: VirtualListState,
  ) {
    const { resources, estimatedItemSize } = nextProps;
    if (resources !== prevState.resources) {
      positions = initPositinoCache(estimatedItemSize, resources.length);

      // 更新高度
      let listHeight = getListHeight(positions);

      // 更新總偏移量
      let startOffset = getStartOffset(prevState.startIndex, positions);

     
      let endIndex = getEndIndex(resources, prevState.startIndex, prevState.visibleCount);
     
      return {
        resources,
        listHeight,
        startOffset,
        endIndex,
      };
    }
    return null;
  }

5 結(jié)束語(yǔ)

好了,一個(gè)完整的vitural list組件完成,該組件因?yàn)槊織l數(shù)據(jù)ItemRender的render函數(shù)時(shí)自定義,所以只要是列表形式,你想虛擬滾動(dòng)誰(shuí),都可以。當(dāng)然,根據(jù)查閱網(wǎng)上的資料,圖片的相關(guān)的滾動(dòng),因?yàn)榫W(wǎng)絡(luò)問題,無(wú)法保證獲取列表項(xiàng)的真是高度,從而可能造成不準(zhǔn)確的情況。這里暫不做討論,有興趣的小伙伴可以再次深入。

到此這篇關(guān)于React虛擬列表的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)React虛擬列表內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • React Render Props共享代碼技術(shù)

    React Render Props共享代碼技術(shù)

    render props是指一種在 React 組件之間使用一個(gè)值為函數(shù)的 prop 共享代碼的技術(shù)。簡(jiǎn)單來(lái)說,給一個(gè)組件傳入一個(gè)prop,這個(gè)props是一個(gè)函數(shù),函數(shù)的作用是用來(lái)告訴這個(gè)組件需要渲染什么內(nèi)容,那么這個(gè)prop就成為render prop
    2023-01-01
  • React控制元素顯示隱藏的三種方法小結(jié)

    React控制元素顯示隱藏的三種方法小結(jié)

    這篇文章主要介紹了React控制元素顯示隱藏的三種方法小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-12-12
  • React組件封裝中三大核心屬性詳細(xì)介紹

    React組件封裝中三大核心屬性詳細(xì)介紹

    這篇文章主要為大家介紹了React組件實(shí)例三大屬性state props refs使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-09-09
  • React?中使用?Redux?的?4?種寫法小結(jié)

    React?中使用?Redux?的?4?種寫法小結(jié)

    這篇文章主要介紹了在?React?中使用?Redux?的?4?種寫法,Redux 一般來(lái)說并不是必須的,只有在項(xiàng)目比較復(fù)雜的時(shí)候,比如多個(gè)分散在不同地方的組件使用同一個(gè)狀態(tài),本文就React使用?Redux的相關(guān)知識(shí)給大家介紹的非常詳細(xì),需要的朋友參考下吧
    2022-06-06
  • Redis數(shù)據(jù)結(jié)構(gòu)面試高頻問題解析

    Redis數(shù)據(jù)結(jié)構(gòu)面試高頻問題解析

    這篇文章主要為大家介紹了Redis數(shù)據(jù)結(jié)構(gòu)高頻面試問題解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-06-06
  • React?Immutable使用方法詳細(xì)介紹

    React?Immutable使用方法詳細(xì)介紹

    Immutable.js出自Facebook,是最流行的不可變數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)之一。它實(shí)現(xiàn)了完全的持久化數(shù)據(jù)結(jié)構(gòu),使用結(jié)構(gòu)共享。所有的更新操作都會(huì)返回新的值,但是在內(nèi)部結(jié)構(gòu)是共享的,來(lái)減少內(nèi)存占用
    2022-09-09
  • React?Native?中限制導(dǎo)入某些組件和模塊的方法

    React?Native?中限制導(dǎo)入某些組件和模塊的方法

    這篇文章主要介紹了React?Native?中限制導(dǎo)入某些組件和模塊的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-08-08
  • React路由管理之React Router總結(jié)

    React路由管理之React Router總結(jié)

    React項(xiàng)目通常都有很多的URL需要管理,最常使用的解決方案就是React Router了,本篇文章主要介紹了React路由管理之React Router總結(jié),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧
    2018-05-05
  • React路由跳轉(zhuǎn)的實(shí)現(xiàn)示例

    React路由跳轉(zhuǎn)的實(shí)現(xiàn)示例

    在React中,可以使用多種方法進(jìn)行路由跳轉(zhuǎn),本文主要介紹了React路由跳轉(zhuǎn)的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下
    2023-12-12
  • React使用refs操作DOM方法詳解

    React使用refs操作DOM方法詳解

    React核心就在于虛擬DOM,也就是在React中不總是直接操作頁(yè)面真實(shí)的DOM元素,并且結(jié)合Diffing算法,可以做到最小化頁(yè)面重繪,有些時(shí)候不可避免的我們需要一種方法可以操作我們定義的元素標(biāo)簽,并作出對(duì)應(yīng)的修改。在React中提供了一種訪問DOM節(jié)點(diǎn)的方式,也就是這里的refs
    2022-11-11

最新評(píng)論