Lazy loading of lists on applets

We often see long lists, why lazy loading of long lists is necessary, because once there is a lot of content rendered, the rendering engine needs more time to render the image, which can result in a blank screen, stutter, etc. In fact, the user only needs to see the contents of the window, rather than rendering the whole content at once, so lazy loading can be implemented.

Paging load

A common lazy list loading is implemented in conjunction with the back end, known as paging loading. The front end requests page number of data, and the back end returns page number of data. The interaction at the front end is to request data from the next page when the user slides to the bottom of the page.

Listen with scroll event




Height diagram

Listen for the scroll event, the event callback will have the current element’s scrolling distance scrollTop, when the scrollTop+screenHeight is equal to the scrollHeight, it indicates that the element has been slid to the bottom.

In applets, the Page object provides the onReachBottomapi

onReachBottom: function() {
  // execute when the page hits the bottom
},
Copy the code

withIntersectionObserverListening to the

Using scrolllistening can be very performance intensive, as it frequently triggers callbacks while scrolling, so calculations are constantly made. A better optimization scheme is IntersectionObserverAPI. If an element monitored by IntersectionObserver intersects with the visible area, a callback will be generated, thus reducing the triggering frequency

Page({
  onLoad: function(){
    wx.createIntersectionObserver().relativeToViewport({bottom: 100}).observe('.target-class'.(res) = > {
      res.intersectionRatio // The proportion of the intersection area to the layout area of the target node, if not equal to 0, it indicates that there is intersection
      res.intersectionRect // The intersection area
      res.intersectionRect.left // The left boundary coordinates of the intersection area
      res.intersectionRect.top // The upper boundary coordinates of the intersecting regions
      res.intersectionRect.width // The width of the intersecting area
      res.intersectionRect.height // The height of the intersection area})}})Copy the code

Front-end paging rendering

All of the above is the front end combined with the interface for paging loading. If the interface is not paginated, it simply returns a large list of data. If the front end directly setData all data, rendering will be very long, in fact, complex list rendering 20 is already slow. At this point, the data that has been captured needs to be paged and rendered in batches.




List paging rendering schematic

As you can see from the right panel, not all nodes are rendered at first, but they are added as you slide through the page.

Go straight to code

<! -- pages/first/index.wxml -->
<view class="container">
  <block wx:for="{{goodsList}}" wx:key="index" wx:for-item="subItemList">
    <block wx:for="{{subItemList}}" wx:key="name">
      <view class="item">{{item.name}}</view>
    </block>
  </block>
</view>
Copy the code

GoodsList is a two-dimensional array that is doubly iterated with wx:for

// pages/first/index.js
const list = Array.from({length: 5}, (item, index) = > {
  return Array.from({length: Math.ceil(Math.random()*10 + 5)}, (subItem, subIndex) = > {
    return {name: The first `${index+1}Screen, the first${subIndex+1}A `}})})/ * * generates a list is a two-dimensional array [[{}, {}], [{}, {}], [{}, {}],... * * /

Page({
  data: {
    goodsList: [].lazyloadIdx: 0
  },
  onLoad() {
    this.setData({
      goodsList: [list[0]],
      lazyloadIdx: 1})},// Add data to goodsList while sliding to the bottom
  onReachBottom () {
    console.log('onReachBottom');
    let { lazyloadIdx } = this.data
    if (lazyloadIdx >= list.length) return
    this.setData({
      [`goodsList[${lazyloadIdx}] `]: list[lazyloadIdx],
      lazyloadIdx: lazyloadIdx+1})}})Copy the code

Only one screen of data is set ata time, which not only reduces the amount of setData data, but also reduces the rendering time

withIntersectionObserverInstead ofonReachBottom

In many scenarios, onReachBottom cannot be used, so we can only use IntersectionObserver instead. Optimize the above code

# pages/second/index.wxml
<view class="container">
  <block wx:for="{{goodsList}}" wx:key="index" wx:for-item="subItemList">
    <block wx:for="{{subItemList}}" wx:key="name">
      <view class="item">{{item.name}}</view>
    </block>
  </block>
+ 
      
</view>
Copy the code

Add nodes for listening

// pages/second/index.js
let lazyloadOb = null
Page({
  data: {
    goodsList: [].lazyloadIdx: 0
  },
  onLoad() {
    this.setData({
      goodsList: [list[0]],
      lazyloadIdx: 1
    })
    this.initObserver()
  },
  onunload () {
    this.disconnenct()
  },
  lazyloadNext () {
    console.log('lazyloadNext');
    let { lazyloadIdx } = this.data
    if (lazyloadIdx >= list.length) return
    this.setData({
      [`goodsList[${lazyloadIdx}] `]: list[lazyloadIdx],
      lazyloadIdx: lazyloadIdx+1
    })
  },
  initObserver () {
    lazyloadOb = wx.createIntersectionObserver().relativeToViewport({ bottom: 50 }).observe('#observer'.(res) = > {
      console.log('res.intersectionRatio', res.intersectionRatio);
      // The next screen is loaded when the callback is triggered
      if (res.intersectionRatio) this.lazyloadNext()
    })
  },
  disconnenct() {
    lazyloadOb.disconnenct()
  }
})
Copy the code

Add demand!

The list of items returned by the back end is just a one-dimensional array that needs to be converted to a two-dimensional array at the front end, and now needs to be 5 in length per screen.

If the number of items in the list is 21, then the length of the corresponding sub-item of the two-dimensional array will be generated:

// pseudo code [5, 5, 5, 5, 1]Copy the code

We can set the pageSize pageSize to 5, the current page pageNum, and then list. Slice (pageNum, pageSize) will intercept the corresponding data and add it to the two-dimensional array.

By default, only non-sold out items + one sold out item will be displayed in the list, and the remaining sold out items will be displayed by clicking the “View More” button

Assuming that there are 16 non-sold out items, 11 sold out items, and the list length per screen is still 5, then:

[
  5.5.5./ / not sold out
  2.// Not sold out + Sold out
  5.5        / / sold out
]
Copy the code

Only two of them aren’t big enough for another screen, so if they’re less than 5, they merge

[
  5.5.7.// Not sold out + one sold out
  5.5     / / sold out
]
Copy the code

So you’re not going to be able to set pageSize, so you’re going to calculate the length array based on the number of sold out, the number of non-sold out, and the length of one screen, and then you’re going to calculate the corresponding two-dimensional array

/ * * *@desc The display list contains the sold out items, splice a sold out item at the end of the non-sold out list, click Expand to load the sold out * *@param {number} OnSaleLen not sold out length *@param {number} SoldOutLen Sold out length *@returns { { subSize: Array<number>; soldOutLen: number } } * /
genSubListSize (onSaleLen, soldOutLen) {
  // When there are sold out items, put a sold out item to the non-sold out side
  if (soldOutLen) {
    soldOutLen-= 1
    onSaleLen+=1
  }
  const arr = []
  const lazyloadListPartLength = 5 // Select * from *
  let firstSize = 0
  if (onSaleLen < lazyloadListPartLength*2) {
    firstSize = onSaleLen
    onSaleLen = 0
  } else {
    firstSize = lazyloadListPartLength
    onSaleLen -= lazyloadListPartLength
  }
  arr.push(firstSize)
  
  // Subitem length
  const size = lazyloadListPartLength
  constremainder = onSaleLen % size arr.push( ... Array.from({length: Math.floor(onSaleLen/size) - 1}, () = > size),
  )
  if (onSaleLen) {
    arr.push(onSaleLen <= size ? onSaleLen : size + remainder)
  }
  // Records the index of sold out items at this point, because you have to click expand to load the sold out list
  const soldOutIndex = arr.length
  if (soldOutLen) {
    constremainder = soldOutLen % size arr.push( ... Array.from({length: Math.floor(soldOutLen/size) - 1}, () = > size), 
      soldOutLen <= size ? soldOutLen : size + remainder
    )
  }

  console.log('genSubListSize', arr)
  
  return {
    subSize: arr,
    soldOutLen,
    soldOutIndex
  }
}
/** * eg: onSaleLen = 25; soldOutLen = 9; size = 5 * return [5, 5, 5, 5, 6, 8] * eg: onSaleLen = 15; soldOutLen = 9; size = 5 * return [5, 5, 6, 8] * eg: onSaleLen = 10; soldOutLen = 10; size = 5 * return [5, 6, 9] * eg: onSaleLen = 14; soldOutLen = 10; size = 5 * return [5, 5, 5, 9] * eg: onSaleLen = 8; soldOutLen = 9; size = 5 * return [9, 8] * eg: onSaleLen = 2; soldOutLen = 10; Return [3, 9] **/
Copy the code

Now take a list length of 20,12 non-sold out, 8 sold out, 5 on one screen

// pages/third/index
const goodsList = Array.from({length: 20}, (item, index) = > {
  return {name: The first `${index+1}A `.soldOut: index < 12 ? false : true}
})
Page({
  // ...
  onLoad() {
    this.initObserver()
    this.handleGoodsList()
  },
  handleGoodsList () {
    const { onSaleLen, soldOutLen } = this.countSaleLen()
    console.log('onSaleLen', onSaleLen, 'soldOutLen', soldOutLen);
    const {
      subSize,
      soldOutLen: soldOutLength,
      soldOutIndex
    } = this.genSubListSize(onSaleLen, soldOutLen)
    const renderList = this.genRenderList(subSize)
    console.log('renderList', renderList);
  },
  countSaleLen () {
    const soldOutIndex = goodsList.findIndex(good= > good.soldOut)
    if (soldOutIndex === -1) {
      return {
        onSaleLen: goodsList.length,
        soldOutLen: 0}}return {
      onSaleLen: soldOutIndex,
      soldOutLen: goodsList.length - soldOutIndex
    }
  },
  // Generate the render list from the grouping array
  genRenderList (subSize) {
    const before = goodsList
    const after = []
    let subList = [] // Subitems of a two-dimensional array
    let subLen = 0 // Subitem length
    let splitSizeArrIdx = 0 // Length array index
    for (let i = 0; i < before.length; i++) {
      if (subLen === subSize[splitSizeArrIdx]) {
        splitSizeArrIdx++
        after.push(subList)
        subList = []
        subLen = 0
      }
      before[i]['part'] = The first `${splitSizeArrIdx+1}Screen `
      subList.push(before[i])
      subLen++
    }
    // Add the last subitem
    after.push(subList)
    return after
  }
})
Copy the code

Print the renderList and you get the data for the dynamic shards




The list of groups

Run the demo




List dynamic grouping lazy loading

Of course, the demand is constantly changing, so it may not be sold out next time, but it is always the same, no matter how to divide, after the array length of each item is set, the renderList can be reproduced. So this lazy piece of logic can be pulled out and encapsulated.

The demo source code

The complete code for the above three demos

reference

  • [1] A small program IntersectionObserver document