微信小程序webview模式瀑布流长列表的虚拟列表优化,解决内存问题

此文章是我看到写的非常好的一篇技术文章,也是好不容搜到的,所以想记录在册。如果原作者看到了不希望转载,请在下面评论,我必删除内容。

这个方式也可以用于skyline模式,但是因为skyline 本身微信小程序已经提供了瀑布流框架,再造轮子就多余了。可以看我写的这篇文章:微信小程序Skyline模式下瀑布长列表优化成虚拟列表,解决内存问题_小程序长列表优化实践-CSDN博客

这个方式对于上面这篇在内存消耗方面,可以减少70%(预估),具体没算,但是肯定更好。因为支撑驻留的view骨架更少。

以下是原文内容:

虽然本篇是写的微信小程序的案例,但是也可用于H5,思路是想通的,只是有些api的差异,最后会贴代码片段

example

虚拟列表

  • 一般在做长列表优化时,特别是面试时,虚拟列表就是个高频词。这个名词听起来很高级,其实原理很简单
  • 虚拟列表就是将需要渲染的数组数据改造成二维数组,然后通过监听DOM节点在合适的地方切换为占位元素,达到长列表无限滚动时减少DOM渲染的优化
  • JS

/**
 * 处理占位元素,就是在获取新的数据后
 * 通过SelectQuery获取当前数据的实际高度,然后把这个高度设置到占位元素上
 */
getCurrentItemHeight() {
  const query = this.createSelectorQuery();
  const { virtualId } = this.data
  query.select(`#${virtualId}`).boundingClientRect()
  query.exec((res) => {
    this.setData({
      height: res[0].height
    }, this.observePage())
  })
}

/**
 * 监听元素与页面的相交
 * 可以选择指定元素为参照区域,也可以选择页面为参照元素,只是API不同
 * @doc https://developers.weixin.qq.com/miniprogram/dev/api/wxml/IntersectionObserver.html
 */
observePage() {
  const { virtualId, observeDistance, wrapId } = this.data
  let IntersectionObserver = wx.createIntersectionObserver(this);
  (wrapId ? IntersectionObserver.relativeTo(`#${wrapId}`) : IntersectionObserver.relativeToViewport({ top: observeDistance, bottom: observeDistance }))
  .observe(`#${virtualId}`, ({ intersectionRatio,isIntersecting }) => {
    this.setData({
      isShow: intersectionRatio > 0&&isIntersecting,
    })
  })
}

WXML:

<view id="{{virtualId}}">
  <block wx:if="{{isShow}}">
    <slot></slot>
  </block>
  <view wx:else style="height: {{ height }}px"></view>
</view>

瀑布流

  • 瀑布流,又称瀑布流式布局。视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部
  • 瀑布流有多种写法,有通过纯CSS完成的,也有借助JS完成的,方法很多,但是我为了接下来能与虚拟列表相结合,就采用JS的写法,就是通过列数把每一列分为一个单独的子元素,然后会记录每一列的高度,通过判断哪一列高度最小,然后将数据push到高度最小的那一列

/**
 * 获取列表数据
 * @describe 瀑布流处理,哪列高度小,就往哪列push新数据
 */
getList() {
    let { listQuery: { pageIndex, pageSize }, columns, columnsHeight } = this.data;
    for (let i = 0; i < pageSize; i++) {
        const height = Math.floor(Math.random() * 100)
        const item = {
          height: height < 150 ? height + 150 : height,
        };
        const position = this.computePosition(columnsHeight)
        columns[position].push(item)
        columnsHeight += item.height
    }
    // 在html中双重遍历columns,然后通过flex:1均匀分布
    this.setData({
      columns,
    })
    this.data.columnsHeight = columnsHeight
}

/**
 * 获取高度最小列下标
 */
computePosition(heights) {
const min = Math.min(...heights);
return heights.findIndex((item) => item === min)
}

瀑布流结合虚拟列表

  • 让瀑布流有虚拟滚动的能力,思路很简单,就是计算每列的偏移量,因为瀑布流被我们分成了二维数组,所以每块子元素之间就会因为列高度的不一致产生空白区域,所以需要计算这个空白区域的大小,然后通过margin-top移动列元素达到视觉上的瀑布流衔接效果

getList() {
  let { listQuery: { pageIndex }, column, columnsHeights } = this.data;
  const columns = [];
  // 上一组的高度数据,用于计算偏移值
  const lastHeights = [...columnsHeights];
  // 获取数据
  const list = this.getListData();
  // 初始化当前屏数据
  for (let i = 0; i < column; i++ ) {
    columns.push([]);
  }
  // 遍历新数据,分配至各列
  for (let i = 0; i < list.length; i++) {
    const position = this.computePosition(columnsHeights);
    columns[position].push(list[i]);
    columnsHeights[position] += Number(list[i].height);
  }
  this.setData({
    [`listData[${pageIndex}]`]: {
      columns,
      columnOffset: this.computeOffset(lastHeights),
    }
  });
  this.data.listQuery.pageIndex = pageIndex + 1;
  this.data.columnsHeights = columnsHeights;
},

/**
 * 获取列表数据
 */
getListData() {
  const result = []
  for (let i = 0; i < this.data.listQuery.pageSize; i++) {
    const height = Math.floor(Math.random() * 300);
    const item = {
      height: height < 150 ? height + 150 : height,
      color: this.randomRgbColor(),
    };
    result.push(item);
  }
  return result;
},

/**
 * 随机生成RGB颜色
 */
randomRgbColor() {
  var r = Math.floor(Math.random() * 256); //随机生成256以内r值
  var g = Math.floor(Math.random() * 256); //随机生成256以内g值
  var b = Math.floor(Math.random() * 256); //随机生成256以内b值
  return `rgb(${r},${g},${b})`; //返回rgb(r,g,b)格式颜色
},

/**
 * 获取最小高度列下标
 */
computePosition(heights) {
  const min = Math.min(...heights);
  return heights.findIndex((item) => item === min);
},

/**
 * 计算偏移量
 */
computeOffset(heights) {
  const max = Math.max(...heights);
  return heights.map((item) => max - item);
},

onScrollLower() {
  this.getList();
}

 WXML

<view>
<scroll-view class="virtualScrollView" eventhandle scroll-y bindscrolltolower="onScrollLower">
  <block wx:for="{{ listData }}" wx:key="screenIndex" wx:for-index="screenIndex" wx:for-item="screenItem">
    <VirtualItem virtualId="virtual_{{screenIndex}}">
      <view class="virtualScreen">
        <block wx:for="{{ screenItem.columns }}" wx:key="columnIndex" wx:for-index="columnIndex" wx:for-item="column" >
          <view style="margin-top: -{{screenItem.columnOffset[columnIndex]}}px;" class="virtualColumn">
            <view class="cell" wx:for="{{column}}" style="height: {{ item.height }}px; background-color: {{ item.color }};" wx:key="index" wx:for-item="item" wx:for-index="index">
              screen: {{ screenIndex }}, column: {{ columnIndex }}
            </view>
          </view>
        </block>
      </view>
    </VirtualItem>
  </block>
</scroll-view>
</view>

这是代码片段地址:demo代码片段

以下是转载文章的我补充需要解决问题的内容:

1、" .cell "之间的间距问题

原文cell是没有间距的,都是黏在一起的,需要改间距的话,在.cell view中加入margin:5px

然后在js 这段代码中columnsHeight[position] += Number(list[i].height); 在height后面加上 margin的2倍,例如:height+(5x2)计算尺寸则会正常,否则因为margin的存在导致top间距越来越大。

2、Skyline模式下使用,目前来看好像是Skyline模式有BUG,导致slot不起作用

直接把内部代码替换到slot里就行

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值