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.
- Applets: Project Environment
- index list: Requirements
- Caton / white screen: Problem
- 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.
- It is necessary to transform the original single-layer structure into double-layer structure
- For the offset scheme, transform conflicts with sticky
- Height of index key
- Multiple index list item s in the visual area
- 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)