Transform index list with virtual list

Keywords: Javascript Front-end Mini Program wechat

introduction

In a team that advocates "rapid development", the delivery date is often the first standard to measure the work. The way to solve problems will also be partial to violence. The way of violence often disgusts and loses voice. Especially when the interviewer asks about the difficulties in the development process, he can't answer. He can only reply "I feel that the development process is very smooth and I haven't encountered any difficult problems.".

The following is the nonviolent way I think of to transform the original problem.

problem

Requirements and problem description

Keywords: applet, index list, Caton, white screen, 500, 1M

In the process of small program project development, we met the demand of index list, so we used vant's IndexBar as the development tool to complete and release it online. However, the problem was finally exposed due to the poor writing of item and the small program bearing. After testing, it is found that when the data is greater than 500, some mobile phones have begun to get stuck, especially when operating (deleting and adding) items; When the data size is greater than 1MB, the first screen will render a white screen for a long time. The IndexList is shown in the following figure.

problem analysis

Because the expression ability is weak, the description above may not be clear, so the keywords are extracted.

  1. Applets: Project Environment
  2. index list: Requirements
  3. Caton / white screen: Problem
  4. Article 500 / 1M: preconditions for problems

From the premise of the question, it is easy to ask the question "can you card with such a small amount of data?". I was also surprised when I found it during the test. What can this data do? In the case of non applet development, I usually meet this piece of code and open a separate project for testing, but the applet is well-known card, so I used a very simple way Baidu "applet list Caton". I didn't even write "long list" when searching, but I still got the result, which is still the first item in the search result. The search results are shown in the figure below.

The problems raised in 2018 were officially solved in 2019 recycle-viewWechat applet long list Caton However, this can only solve some problems, and may not be suitable for nested data. And the internal implementation is also operated according to the idea of virtual list rendering.

Scheme and Implementation

In the subsequent scheme, the implementation details and environment will be changed to browser environment and encoded by Vue.

ps: vite + vue is too slippery in writing demo.

premise

It is difficult for individuals to use applet development tools for coding. Considering that the cost of scheme, implementation and migration is relatively low, the subsequent implementation adopts the browser to transplant the applet.

Development environment: vscode + vite + vue

mock data

domo environment, using mock data to provide data support for subsequent development.

ps: the order of keys will not be considered for the time being

mock structure

{
    "A":[ ... ],
    ...
    "Z":[ ... ]
}

mock generates the following code.

import { Random } from 'mockjs'

export const indexListData = Array(26).fill('A'.codePointAt()).reduce((pv, indexCode, index) => {
  const currentCharAt = indexCode + index
  const currentChar = String.fromCharCode(currentCharAt)
  pv[currentChar] = Array(Math.random() * 460 | 0).fill(0).map((_, itemIndex) => {
    const id = currentCharAt + '-' + itemIndex
    return {
      id,
      index: currentChar,
      pic: "https://image.notbucai.com/logo.png",
      title: Random.ctitle(5, 20),
      group: id,
      content: Random.ctitle(100, 150),
      user: {
        id: 123,
        name: 'No talent',
        avatar: 'https://image.notbucai.com/logo.png',
        age: 12,
        sex: 1,
      },
      createAt: Date.now(),
      updateAt: Date.now(),
    }
  })
  return pv;
}, {})

Business code

Rendering

[external chain picture transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-xyd6hc6d-1634549067882)( https://i.loli.net/2021/10/18/H75FIuBvzsGmgtD.jpg )]

There is no code before the transformation. Only partially implemented, not fully implemented

<template>
  <div class="list-page-box">
    <div class="list-box">
      <div class="group-box" v-for="(value, key) of list" :key="key">
        <div class="gropu-index">{{ key }}</div>
        <div class="group-content">
          <div class="group-item" v-for="item in value" :key="item.id">
            <img
              class="group-item-pic"
              :src="item.pic"
              alt="123"
              loading="lazy"
            />
            <div class="group-item-content">
              <h1>{{ item.title }}</h1>
              <p>{{ item.content }}</p>
            </div>
            <div class="group-item-aciton">
              <button>delete</button>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="index-field-box">
      <div class="index-key-name" v-for="(_, key) of list" :key="key">
        {{ key }}
      </div>
    </div>
  </div>
</template>

<script>
import { reactive } from "vue"
import { indexListData } from "./mock"

export default {
  setup () {
    const list = reactive(indexListData)

    return {
      list
    }
  },
}
</script>

<style lang="scss" >
* {
  padding: 0;
  margin: 0;
}
.list-page-box {
  position: relative;
}
.list-box {
  .group-box {
    margin-bottom: 24px;
    .gropu-index {
      background-color: #f4f5f6;
      padding: 10px;
      font-weight: bold;
      position: sticky;
      top: 0;
    }
    .group-content {
      .group-item {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 6px 10px;

        .group-item-pic {
          width: 68px;
          min-width: 68px;
          height: 68px;
          margin-right: 12px;
        }
        .group-item-content {
          display: flex;
          flex-direction: column;
          height: 100%;
          h1 {
            font-size: 16px;
            font-weight: bold;
            color: #333333;
          }
          p {
            color: #666666;
            font-size: 14px;
          }
        }

        .group-item-aciton {
          min-width: 60px;
          display: flex;
          align-items: center;
          justify-content: end;
        }
      }
    }
  }
}
.index-field-box {
  position: fixed;
  top: 0;
  right: 0;
  z-index: 10;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
</style>

programme

Use virtual list, refer to Cloud Bridge - "advanced front end" high performance rendering 100000 pieces of data (virtual list) A new scheme.

According to the above description of the virtual list, a simple virtual list is written. The code is as follows.

<template>
  <div class="list-page-box" ref="scrollRef">
    <!--Temporarily fixed height-->
    <div style="height: 10000px"></div>
     <!--list-->
    <div class="list-box" :style="{ transform: listTransform }">
      <div class="item" v-for="item in list" :key="item">{{ item }}</div>
    </div>
  </div>
</template>

<script>
import { computed, onMounted, reactive, ref } from "vue"

export default {
  setup () {
    const scrollRef = ref(null)
    const listTransform = ref("translate3d(0px, 0px, 0px)")
    // Generate data
    const originList = reactive(Array(10000).fill(0).map((_, index) => index))
    const startIndex = ref(0)
    
    const list = computed(() => {
      return originList.slice(startIndex.value, startIndex.value + 10)
    })

    onMounted(() => {
      scrollRef.value.addEventListener('scroll', () => {
        const scrollTop = scrollRef.value.scrollTop
        // Calculate the start position of the list
        const start = scrollTop / 83 | 0
        startIndex.value = start;
        // Calculate offset
        listTransform.value = `translate3d(0px, ${((start * 83)).toFixed(2)}px, 0px)`
      })
    })

    return {
      list,
      scrollRef,
      listTransform
    }
  },
}
</script>

<style lang="scss" >
.list-page-box {
  position: relative;
  height: 100vh;
  width: 100vw;
  overflow: hidden;
  overflow-y: auto;
}
.list-box {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}
.item {
  padding: 30px;
  border-bottom: 1px solid #000;
}
</style>

Transformation difficulties

The main problem in this transformation is that it is currently a nested data list.

  1. It is necessary to transform the original single-layer structure into double-layer structure
  2. For the offset scheme, transform conflicts with sticky
  3. Height of index key
  4. Multiple index list item s in the visual area
  5. Click the Index Key on the right to jump to the specified position

realization

The subsequent transformation and implementation are carried out through the virtual list code above. The implementation code is put here first, and the above problems will be solved later.

<template>
  <div class="list-page-box" ref="scrollRef">
    <div :style="{ height: scrollHeight + 'px' }"></div>
    <!-- fix: Replace the solution to problem 3 with top Solve it temporarily -->
    <div class="list-box" :style="{ top: offsetTop + 'px' }">
      <div class="group-box" v-for="(value, key) of list" :key="key">
        <div class="gropu-index">{{ key }}</div>
        <div class="group-content">
          <div class="group-item" v-for="item in value" :key="item.id">
            <div class="group-item-pic" style="background: #f5f6f7"></div>
            <div class="group-item-content">
              <h1>{{ item.title }}</h1>
              <p>{{ item.content }}</p>
            </div>
            <div class="group-item-aciton">
              <button>delete</button>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="index-field-box">
      <div
        class="index-key-name"
        v-for="key in keys"
        :key="key"
        @click.stop.prevent="handleToList(key)"
      >
        {{ key }}
      </div>
    </div>
  </div>
</template>

<script>
import { computed, onMounted, reactive, ref, watch, watchEffect } from "vue"
import { indexListData } from "../mock"

// mock index list data
console.log('indexListData', JSON.stringify(indexListData));
// todo encapsulation problem (other problems after data update will not be considered temporarily)
// Let's look at some excellent packages first
// It feels like an idea 
// Incoming data - > slot item, then I'm too lazy to seal the md slacker
// 1. Input
//   Data index list item height
// 2. Output
//   Initialized function
//   Rendered data
export default {
  setup () {
    // Original data
    const originList = indexListData
    
    const scrollRef = ref(null)
    const scrollTop = ref(0) // todo requires additional offset calculations
    // The height of the final rendering of the stored data
    const scrollHeight = ref(0)
    const offsetTop = ref(0)
    // Current subscript
    const showListIndexs = reactive({
      key: 'A',
      index: 0,
      sonIndex: 0
    });

    // Temporary storage
    const originListHeight = ref([])
    const keys = ref([])

    // Data to render
    const list = computed(() => {
      // Get key 
      const { key, index, sonIndex } = showListIndexs;
      // get data 
      // todo, the 10 elements here need to be calculated later. It doesn't matter at present
      const showList = originList[key].slice(sonIndex, sonIndex + 10)
      // todo actually has some problems with the current key: value organization (out of order). This is not listed in the following table for the time being
      const showData = {
        [key]: showList
      }
      // Processing when the length of calculation data is not enough
      // todo needs to be refined
      if (showList.length < 10) {
        // Dealing with problems when data is insufficient
        const nextIndex = index + 1

        if (nextIndex >= originListHeight.value.length) return showData
        const nextHeightData = originListHeight.value[nextIndex];
        if (!nextHeightData) return showData;
        const nextKey = nextHeightData.key;
        const nextShowList = originList[nextKey].slice(0, 10 - showList.length)
        showData[nextKey] = nextShowList
      }

      return showData
    })

    // Monitor data
    onMounted(() => {
      scrollRef.value.addEventListener('scroll', () => {
        const _scrollTop = scrollRef.value.scrollTop
        // todo height calculation
        // The height offset needs to be combined with data update to complete the rolling interaction
        scrollTop.value = _scrollTop
      })
    })

    // Asynchronous triggering can be replaced later in a life cycle
    onMounted(() => {
      let total = 0;
      for (let key in originList) {
        const value = originList[key]
        // todo temporary borrowing
        keys.value.push(key)

        originListHeight.value.push({
          index: 42,
          list: value.length * 80,
          total: value.length * 80 + 42,
          key
        })
        total += value.length * 80 + 42
      }
      scrollHeight.value = total
    })

    // Only focus on the changes of scrollTop
    watchEffect(() => {
      // Separate the calculation process to reduce the list update meaningless rendering
      // index is mainly calculated here
      if (originListHeight.value.length == 0) {
        // Assign values separately to reduce meaningless list rendering
        showListIndexs.key = 'A'
        showListIndexs.index = 0
        showListIndexs.sonIndex = 0
        return
      }
      // todo 
      // scrollTop through scrollTop 
      // Before calculation, you need to calculate the height of the original data (originList)
      // The problems caused by PX - > rem are not considered at present
      // Through the css set, you can know an item height: 80px;
      // However, you also need to know the height of indxKey, that is, the height of class = "gropu index": 42px;
      // In the early stage of todo, the unit is fixed, the core is completed first, and then the dynamic height is considered
      // 1. Find the general direction, that is, the first layer data
      // 2. Calculate the sub data Index after subtracting scrollTop from the general direction 
      // 3. The data is not enough. You need to get the lower level data
      let total = 0;
      let index = originListHeight.value.findIndex(item => {
        // Find the height and the first higher than the current scroll height
        let t = total + item.total
        if (t > scrollTop.value) {
          return true;
        }
        total = t;
        return false;
      });
      // Handle the case where top is 0 for the first time
      // todo, there's a little problem here. I'll explain it later
      if (index === -1) return {
        key: 'A',
        sonIndex: 0
      };
      const key = originListHeight.value[index].key;
      // total is the most recent
      const sonListTop = scrollTop.value - total
      // Get sub list start subscript
      const sonIndex = sonListTop / 80 | 0
      // console.log('sonIndex',sonIndex);
      // Calculate offset ok
      offsetTop.value = total + sonIndex * 80;

      showListIndexs.key = key
      showListIndexs.index = index
      showListIndexs.sonIndex = sonIndex
    }, [scrollTop])

    return {
      list,
      scrollRef,
      scrollTop,
      scrollHeight,
      offsetTop,
      keys,
      handleToList (key) {
        // Since the height of pre rendering has been calculated after data loading
        // Therefore, all other operations can be completed by changing the scroll height
        if (!scrollRef.value) return;
        // Calculated height
        let height = 0;

        const heightData = originListHeight.value.find(item => {
          if (item.key === key) return true;
          height += item.total;
          return false;
        })
        if (!heightData) return;
        scrollRef.value.scrollTo(0, height)
      }
    }
  },
}
</script>

<style lang="scss" >
* {
  padding: 0;
  margin: 0;
}
.list-page-box {
  position: relative;
  height: 100vh;
  width: 100vw;
  overflow: hidden;
  overflow-y: auto;
}

.list-box {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  .group-box {
    /* padding-top: 24px; */
    box-sizing: border-box;
    .gropu-index {
      background-color: #f4f5f6;
      padding: 10px;
      font-weight: bold;
      // todo bug
      position: sticky;
      top: 0;
      height: 42px;
      box-sizing: border-box;
    }
    .group-content {
      .group-item {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 6px 10px;
        // Fixed height
        height: 80px;
        box-sizing: border-box;
        /* No other treatment shall be done to ensure the consistency of height */
        overflow: hidden;

        .group-item-pic {
          width: 68px;
          min-width: 68px;
          height: 68px;
          margin-right: 12px;
        }
        .group-item-content {
          display: flex;
          flex-direction: column;
          height: 100%;
          h1 {
            font-size: 16px;
            font-weight: bold;
            color: #333333;
          }
          p {
            color: #666666;
            font-size: 14px;
          }
        }

        .group-item-aciton {
          min-width: 60px;
          display: flex;
          align-items: center;
          justify-content: end;
        }
      }
    }
  }
}
.index-field-box {
  position: fixed;
  top: 0;
  right: 0;
  z-index: 10;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 10px;
}
</style>

Difficulty solving

Render position and offset position

Since a single IndexList in double-layer data contains the height of Index and List, the data height is predicted after obtaining the data. Here, the prediction method is fixed item and key height.

Premise: item height is 80 and index height is 42; Here, you can perform pre rendering first, and then get the rendering height.

Height calculation

// The total height is used to fix the scroll height
let total = 0;
// Cycle all heights
for (let key in originList) {
    const value = originList[key]
    // Record the rendering you want to use for the list on the right
    keys.value.push(key)
    // cache
    originListHeight.value.push({
      index: 42,
      list: value.length * 80,
      total: value.length * 80 + 42,
      key
    })
    total += value.length * 80 + 42
}
scrollHeight.value = total

The calculation of rendered data is based on the scroll position and data height. For the rendered data, only the data subscripts of the first layer and the second layer need to be calculated respectively.

For the first layer, you only need to calculate the scroll height and data height.

The difference between the second layer position and the first layer data height and rolling height is obtained, and then the height of a single element is removed.

// Only focus on the changes of scrollTop
watchEffect(() => {
  // Separate the calculation process to reduce the list update meaningless rendering
  // index is mainly calculated here
  if (originListHeight.value.length == 0) {
    // Assign values separately to reduce meaningless list rendering
    showListIndexs.key = 'A'
    showListIndexs.index = 0
    showListIndexs.sonIndex = 0
    return
  }

  // Find the first layer data location
  let total = 0;
  let index = originListHeight.value.findIndex(item => {
    // Find the height and the first higher than the current scroll height
    let t = total + item.total
    if (t > scrollTop.value) {
      return true;
    }
    total = t;
    return false;
  });
  // Handle the case where top is 0 for the first time
  // todo, there's a little problem here. I'll explain it later
  if (index === -1) return {
    key: 'A',
    sonIndex: 0
  };
  const key = originListHeight.value[index].key;
  // total is the most recent
  const sonListTop = scrollTop.value - total
  // Get sub list start subscript
  const sonIndex = sonListTop / 80 | 0
  // console.log('sonIndex',sonIndex);
  // Calculate offset ok
  offsetTop.value = total + sonIndex * 80;

  showListIndexs.key = key
  showListIndexs.index = index
  showListIndexs.sonIndex = sonIndex
}, [scrollTop])

Calculation of rendering data

The calculation attribute is updated according to the change of showlistindexes. After calculating the position through scrollTop, the first and second layer subscripts are obtained for data interception. However, the change of rolling position may cause the second layer data to be unable to meet the rendering of the whole visual area. Therefore, additional data is needed to supplement the calculation. The supplementary calculation here is only two layers for the time being.

// Data to render
const list = computed(() => {
  // Get key 
  const { key, index, sonIndex } = showListIndexs;
  // get data 
  // todo, the 10 elements here need to be calculated later. It doesn't matter at present
  const showList = originList[key].slice(sonIndex, sonIndex + 10)
  // todo actually has some problems with the current key: value organization (out of order). This is not listed in the following table for the time being
  const showData = {
    [key]: showList
  }
  // Processing when the length of calculation data is not enough
  // todo needs to be refined and needs a loop
  if (showList.length < 10) {
    // Dealing with problems when data is insufficient
    const nextIndex = index + 1

    if (nextIndex >= originListHeight.value.length) return showData
    const nextHeightData = originListHeight.value[nextIndex];
    if (!nextHeightData) return showData;
    const nextKey = nextHeightData.key;
    const nextShowList = originList[nextKey].slice(0, 10 - showList.length)
    showData[nextKey] = nextShowList
  }

  return showData
})

Click jump on the right

Since the pre rendering height is calculated in advance, this problem is approximately non-existent.

// Since the height of pre rendering has been calculated after data loading
// Therefore, all other operations can be completed by changing the scroll height
if (!scrollRef.value) return;
// Calculated height
let height = 0;

const heightData = originListHeight.value.find(item => {
  if (item.key === key) return true;
  height += item.total;
  return false;
})
if (!heightData) return;
scrollRef.value.scrollTo(0, height)

Transplantation problem

The transplantation of general functions can be completed only by replacing the listening and scrolling positions. So there is no detailed description here.

reference resources

Front end advanced high performance rendering 100000 pieces of data (virtual list)

Posted by n8r0x on Mon, 18 Oct 2021 13:38:04 -0700