The introduction

On a “fast development” team, delivery dates are often the first yardstick for measuring work. And the solution to the problem will be violent, violent way often brain will hate and lose voice, especially when the interviewer asked the difficult point in the development process is unable to answer, can only have no confidence to reply, “I feel the development process is smooth, there is no difficult problem to solve.” .

Here are my ideas for nonviolent ways to change the problem.

The problem

Description of requirements and problems

Keywords: small program, index list, katen, white screen, 500, 1M

In the process of small program project development, I met the need of index list, so I used Vant IndexBar as the development, completed and published online. However, due to the poor writing of Item and the load of small program, the problems were finally exposed. Through the test, it is found that when the data is more than 500, some mobile phones have started to suffer from lag, among which the lag is obvious when item is operated (delete, add). When the data size is larger than 1MB, the first screen rendering will be white for a long time. IndexList is shown in the figure below.

Problem analysis

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

  1. Small program: Project Environment
  2. index listDemand:
  3. Caton/blank screenProblem:
  4. Article 500 a / 1 m: The premise of the problem

From the premise of the question, it is easy to generate a question “how can such a small amount of data be jammed?” . I also felt surprised when I found out in the testing process, what can this data do? In the case of the small program development I usually meet with this piece of code to open a project, but small program known as card, so I took a very simple way baidu “small program list caton”, in the search when I didn’t even write “long list”, but I still got the result, or the first in the search results. The search results are shown below.

The problem was raised in 2018, and the official solution was presented in 2019. Recycle-view wechat mini program long list is stuck, but this can only solve part of the problem, and may not be suitable for nested data. And the internal implementation is also according to the idea of virtual list rendering to operate.

Scheme and Implementation

In the future, the implementation details and environment will be changed to browser environment and Vue will be used for coding.

Ps: Vite + Vue is too slippery to write a demo.

The premise

Using applets development tools for coding is uncomfortable for individuals, considering the scheme and implementation and migration costs are relatively low, so the subsequent implementation of the browser implementation after the transplantation of applets.

Development environment: vscode + vite + vue

The mock data

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

Ps: Ignore the order of keys for the moment

The mock structure


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

Copy the code

The 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 + The '-' + 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: 'ineffective'.avatar: 'https://image.notbucai.com/logo.png'.age: 12.sex: 1,},createAt: Date.now(),
      updateAt: Date.now(),
    }
  })
  return pv;
}, {})
Copy the code

Business code

rendering

No modification of the previous code. Only partially implemented, not completely 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> </button> </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>Copy the code

plan

Adopt virtual list, refer to cloud Bridge – “front-end advanced” high performance rendering 100000 data (virtual list) scheme.

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

<template> <div class="list-page-box" ref="scrollRef"> <! - temporary fixed height -- > < div style = "height: 10000 px" > < / div > <! <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)") 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. The scrollTop / / calculate start const start list = scrollTop / 83 | 0 startIndex. Value = start; 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>Copy the code

Transform the difficulty

The main problem with this transformation is that there is currently a nested list of data.

  1. The original single-layer structure needs to be transformed into a double-layer structure
  2. In the offset scheme, transform conflicts with sticky
  3. Index key height problem
  4. Multiple Index List items in the viewable area
  5. Click Index Key on the right to jump to the specified location

implementation

Through the above virtual list code for subsequent transformation and implementation, here first put the implementation code, will solve the above problems respectively.

<template>
  <div class="list-page-box" ref="scrollRef">
    <div :style="{ height: scrollHeight + 'px' }"></div>
    <!-- fix: 问题 3 的解决方案 更换成 top 临时解决一下 -->
    <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>删除</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 封装 问题 (暂时不考虑数据更新后的其他问题)
// 先去看一些优秀的封装
// 感觉都是一个想法 
// 传入 数据 -> slot item 这样的话我就懒得封了 md 懒鬼
// 1. 输入
//   数据 index 高度 list item 高度
// 2. 输出
//   初始化的函数
//   渲染的数据
export default {
  setup () {
    // 原数据
    const originList = indexListData
    
    const scrollRef = ref(null)
    const scrollTop = ref(0) // todo 需要额外计算偏移
    // 存储数据最终渲染的高度
    const scrollHeight = ref(0)
    const offsetTop = ref(0)
    // 当前下标
    const showListIndexs = reactive({
      key: 'A',
      index: 0,
      sonIndex: 0
    });

    // 临时存储
    const originListHeight = ref([])
    const keys = ref([])

    // 需要渲染的数据
    const list = computed(() => {
      // 获取key 
      const { key, index, sonIndex } = showListIndexs;
      // 获取数据 
      // todo 这里的10个元素 后期需要进行计算 目前无所谓
      const showList = originList[key].slice(sonIndex, sonIndex + 10)
      // todo 实际上目前的key: value的机构还是有些问题的(无序),这个暂时按下不表
      const showData = {
        [key]: showList
      }
      // 计算 数据长度不够时的处理
      // todo 需要再细致化
      if (showList.length < 10) {
        // 处理 数据不够时的问题
        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
    })

    // 监听数据
    onMounted(() => {
      scrollRef.value.addEventListener('scroll', () => {
        const _scrollTop = scrollRef.value.scrollTop
        // todo 高度计算
        // 高度偏移需要配合上数据更新才能完成滚动的交互
        scrollTop.value = _scrollTop
      })
    })

    // 用一个生命周期 后期可换成 异步触发
    onMounted(() => {
      let total = 0;
      for (let key in originList) {
        const value = originList[key]
        // todo 临时借用
        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
    })

    // 只关注 scrollTop 的变化
    watchEffect(() => {
      // 分离一下 计算过程 减少列表更新 无意义渲染
      // 这里主要计算 index
      if (originListHeight.value.length == 0) {
        // 分别赋值 减少无意义的list渲染
        showListIndexs.key = 'A'
        showListIndexs.index = 0
        showListIndexs.sonIndex = 0
        return
      }
      // todo 
      // scrollTop 通过scrollTop 
      // 计算之前需要计算原数据(originList)的高度
      // 目前不考虑 px -> rem 造成的问题
      // 通过设置的css可知一个item height: 80px;
      // 但是还需要知道indxKey也就是 class="gropu-index"的高度 height: 42px;
      // todo 前期单位固定 先完成核心 再考虑动态高度的问题
      // 1. 找到大方向 也就是 首层数据
      // 2. 根据大方向 减去 scrollTop 后 计算子数据Index 
      // 3. 数据不够需要 拿到下层数据
      let total = 0;
      let index = originListHeight.value.findIndex(item => {
        // 找到高度和比当前滚动高度 大的第一个
        let t = total + item.total
        if (t > scrollTop.value) {
          return true;
        }
        total = t;
        return false;
      });
      // 处理 首次 top 为0的情况
      // todo 这里还有点小问题 晚点说明
      if (index === -1) return {
        key: 'A',
        sonIndex: 0
      };
      const key = originListHeight.value[index].key;
      // total 为最近的
      const sonListTop = scrollTop.value - total
      // 得到子列表开始下标
      const sonIndex = sonListTop / 80 | 0
      // console.log('sonIndex',sonIndex);
      // 计算偏移 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) {
        // 由于数据加载后已经对预渲染的高度进行了一个计算
        // 所以这里只要改变滚动的高度即可完成其他所有操作
        if (!scrollRef.value) return;
        // 计算高度
        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;
        // 固定的高度
        height: 80px;
        box-sizing: border-box;
        /* 不做其他处理 保证高度一致 */
        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>
Copy the code

The difficulty to solve

Render position and offset position

As a single IndexList contains the height of Index and List in two-layer data, the height of the data is predicted first after the data is obtained. Here, the prediction method is fixed item and key height.

Premise: item height is 80, index height is 42; Here you can then pre-render and get the height of the render.

Height calculation

// The total scroll height is used to fix scroll height
let total = 0;
// Loop through all heights
for (let key in originList) {
    const value = originList[key]
    // Record the desired key for the rendering of 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
Copy the code

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

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

The second layer position takes the difference from the first layer data height and scroll height and removes the height of individual elements.

// Focus only on scrollTop changes
watchEffect(() = > {
  // Separate the calculation process to reduce the list update nonsense rendering
  // Here is the index
  if (originListHeight.value.length == 0) {
    // Separate assignments reduce meaningless list rendering
    showListIndexs.key = 'A'
    showListIndexs.index = 0
    showListIndexs.sonIndex = 0
    return
  }

  // Find the level 1 data location
  let total = 0;
  let index = originListHeight.value.findIndex(item= > {
    // Find the height and the first one greater than the current scroll height
    let t = total + item.total
    if (t > scrollTop.value) {
      return true;
    }
    total = t;
    return false;
  });
  // Handle the first case where top is 0
  // There are a few minor issues with todo that will be explained later
  if (index === -1) return {
    key: 'A'.sonIndex: 0
  };
  const key = originListHeight.value[index].key;
  // total is the latest
  const sonListTop = scrollTop.value - total
  // Get the sublist start index
  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])
Copy the code

Calculation of render data

The calculation attribute is used to update according to the change of showListIndexs. After calculating the position through scrollTop, the first and second subscripts are obtained for data interception. However, the change of the scrolling position leads to the second data may not be able to meet the requirements of rendering the entire visual area. Therefore, additional data supplement calculation is needed. Here, only two layers of supplement calculation are done for the time being.

// Data to render
const list = computed(() = > {
  / / get the key
  const { key, index, sonIndex } = showListIndexs;
  // Get data
  // Todo here 10 elements need to be calculated later, so it doesn't matter now
  const showList = originList[key].slice(sonIndex, sonIndex + 10)
  // Todo actually has some problems with the current key: value mechanism
  const showData = {
    [key]: showList
  }
  // Calculate the length of the data is not enough
  // Todo needs to be refined again and needs a loop
  if (showList.length < 10) {
    // Handle the problem of insufficient data
    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
})
Copy the code

Right click jump

Since the pre-render height is calculated in advance, this problem is approximately nonexistent.

// The pre-rendered height has been calculated after the data is loaded
// All you need to do is change the height of the scroll
if(! scrollRef.value)return;
// Calculate the 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)
Copy the code

Transplant problem

Just replace the listener and scroll position to complete the migration of the general functionality. So I’m not going to go into detail here.

reference

High performance rendering of 100,000 pieces of data (virtual list)