JSFiddle - React, Tailwind, and code Playground

by Lu Max

HTML

<script src="//unpkg.com/[email protected]/dist/vue.js"></script>
<script type="text/x-template" id="list-template">
  <div 
    class="list-view" 
    ref="list" 
    @scroll="handleScroll">
    <div     
      class="list-view-phantom"       
      :style="{
         height: contentHeight + 'px'
      }">
    </div>
    <div
      ref="content"
      class="list-view-content">
      <div
        class="list-view-item"
        :style="{
          height: getItemSizeAndOffset(startIndex + index).size + 'px'
        }"
        :key="index"
        v-for="(item, index) in visibleData">
        {{ item.value }}
      </div>
    </div>
  </div>
</script>
<div id="app">
<template>
  <list-view 
    :item-size-getter="itemSizeGetter"
    :estimated-item-size="30"  
    :data="data"></list-view>
</template>
</div>

SCSS

.list-view {
  height: 400px;
  overflow: auto;
  position: relative;
  color: #333;
  border: 1px solid #aaa;
}

.list-view-phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.list-view-content {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
}

.list-view-item {
  padding: 5px;
  color: #666;
  line-height: 30px;
  box-sizing: border-box;
}

Babel + JSX

const ListView = {
	name: 'ListView',

  template: '#list-template',
	
	props: {
  	data: {
    	type: Array,
      required: true
    },

    estimatedItemSize: {
      type: Number,
      default: 30
    },

    itemSizeGetter: {
      type: Function
    }
  },
  
  computed: {
  	contentHeight() {
			const { data, lastMeasuredIndex, estimatedItemSize } = this;
      let itemCount = data.length;
      if (lastMeasuredIndex >= 0) {
        const lastMeasuredSizeAndOffset = this.getLastMeasuredSizeAndOffset();
        return lastMeasuredSizeAndOffset.offset + lastMeasuredSizeAndOffset.size + (itemCount - 1 - lastMeasuredIndex) * estimatedItemSize;
      } else {
        return itemCount * estimatedItemSize;
      }
    }
  },

  mounted() {
    this.updateVisibleData();
  },

  data() {
    return {
    	lastMeasuredIndex: -1,
      startIndex: 0,
      sizeAndOffsetCahce: {},
      visibleData: []
    };
  },

  methods: {
  	getItemSizeAndOffset(index) {
      const { lastMeasuredIndex, sizeAndOffsetCahce, data, itemSizeGetter } = this;
      if (lastMeasuredIndex >= index) {
        return sizeAndOffsetCahce[index];
      }

      let offset = 0;
      if (lastMeasuredIndex >= 0) {
        const lastMeasured = sizeAndOffsetCahce[lastMeasuredIndex];
        if (lastMeasured) {
          offset = lastMeasured.offset + lastMeasured.size;
        }
      }

      for (let i = lastMeasuredIndex + 1; i <= index; i++) {
        const item = data[i];
        const size = itemSizeGetter.call(null, item, i);
        sizeAndOffsetCahce[i] = {
          size,
          offset
        };
        offset += size;
      }
      this.lastMeasuredIndex = index;
      return sizeAndOffsetCahce[index];
    },
    
    getLastMeasuredSizeAndOffset() {
      return this.lastMeasuredIndex >= 0 ? this.sizeAndOffsetCahce[this.lastMeasuredIndex] : { offset: 0, size: 0 };
    },
    
    
    binarySearch(low, high, offset) {
      let index;

      while (low <= high) {
        const...