评论

【改进版】如何从零实现上拉无限加载瀑布流组件

瀑布流组件改进版,通俗易懂。

前言

之前分享过一篇瀑布流如何实现的文章,经过时间的证明,之前的做法并不好,性能上会有问题,所以还是不投机取巧了,老老实实的实现。

回顾:

  • 通过grid-auto-rows的特性实现
  • item通过grid-row设置高度
  • js获取节点高度计算span的值
  • 通过wxs设置css的变量实现修改样式

痛点:

  • grid-auto-rows数值越大,span计算准确度越低。
  • 谷歌浏览器、微信开发工具,如果界面高度超过1000 * grid-auto-rows的高度,那么后面的内容就不会显示了,谷歌解释说是为了不过渡消耗性能。
  • 因为性能问题,超过1000的item就不会显示了,全会挤压在最下面,导致页面非常卡,开发工具能直接卡崩溃,手机上还没发现这个问题,之前也忽视了这个问题,后面调试的时候就非常恼火,开发工具跟真机上效果不一致。
  • 为了保证span计算的准确度高,grid-auto-rows一般设置成1-10px,1px准确就等于view的高度,但是超过1000px就卡没了。

实现思路

  1. 通过selectAllComponents获取所有的子节点
  2. 通过getComputedStyle获取节点的高度
  3. 简单的排序算法计算节点位置
  4. 设置节点的样式
  5. 通过wxs的getState储存每屏节点渲染的数据
  6. 触发image组件的load事件重新计算并渲染节点位置

创建组件

需要开启抽象节点

// waterfall/index.json
{
	"componentGenerics": {
		"selectable": true
	}
}

利用wxs响应事件获取页面的节点

<view
	class="waterfall"
	views="{{ views.length }}"
	data-option="{{ {span} }}"
	change:views="{{ wxs.init }}"
>
	<!-- 嵌套遍历views二维数组 -->
	<block
		wx:for="{{ views }}"
		wx:key="item"
		wx:for-index="i"
	>
		<selectable
			class="item view-{{ i }}"
			wx:for="{{ item }}"
			wx:key="item"
			value="{{ item }}"
		/>
	</block>
</view>

创建item的x,y边距变量 --span

.waterfall {
  --span: 5px;
  width: 100%;
  position: relative;

  .item {
    width: calc(50% - var(--span));
    position: absolute;
  }
}

创建 index.wxs,核心业务代码都写在这里

// 当views被setData的时候会被触发
module.export = {
	init: function(newValue, oldValue, ownerInstance, instance) {
		console.log(newValue, oldValue, ownerInstance, instance)
	}
}

业务逻辑

步骤一:获取所有节点

function init(length, oldValue, ownerInstance, instance) {
	// 加个判断,避免views长度为0时,或者长度为发生变化时也会执行业务代码
	// 只有当views被push新的内容才会执行下面的业务
	if (!length || length === oldValue) return

	// index 其实就是views的长度减一,就等于当前的数组下标
	var index = length - 1
	var views = ownerInstance.selectAllComponents('.view-' + index)
	console.log(JSON.stringify(views))
}


步骤二:遍历views获取节点的高度

views.forEach(function(v, k){
	var viewStyle = v.getComputedStyle(['width', 'height'])
	// 获取高度
	var height = viewStyle.height
	console.log(viewStyle)
	// [WXS Runtime info] {"width":"182.5px", "height":"242px"} 
})

步骤三:计算view的位置信息

var LH = 0
var RH = 0
views.forEach(function (v, k) {
	var viewStyle = v.getComputedStyle(['width', 'height'])
	// 格式化高度,将px去掉
	var height = fixUnit(viewStyle.height)
	var style = {}
	if (LH <= RH) {
		style = { left: 0, top: LH + 'px' }
		LH += height
	} else if (RH < LH) {
		style = { right: 0, top: RH + 'px' }
		RH += height
	}
	// 设置view的样式
	v.setStyle(style)
})

此时,页面的节点会根据position自动排列好

步骤四:储存LH,RH到局部变量

function init(length, oldValue, ownerInstance, instance) {
  if (!length || length === oldValue) return
  // 获取局部变量
  var state = ownerInstance.getState()
  // 获取当前节点的dataset
  var dataset = instance.getDataset()
  var index = length - 1

  state.option = dataset.option
  state.page = length

  // 创建并生成记录左侧、右侧高度
  // 用二维数组来记录
  if (!state.heights) {
    state.heights = [[0, 0]]
  }
  // 记录初次渲染时间戳
  if (!state.timeouts) {
    state.timeouts = []
  }
  // 获取时间戳,并且加上3000毫秒,用于后面计算图片loaded完是否超时
  state.timeouts[index] = getDate().getTime() + 3000

  refreshViews(index, ownerInstance, state)
}

function refreshViews(index, ownerInstance, state) {
  var views = ownerInstance.selectAllComponents('.view-' + index)
  var span = state.option.span

  var LH = state.heights[index][0] // 左侧
  var RH = state.heights[index][1] // 右侧

  views.forEach(function (v, k) {
    var viewStyle = v.getComputedStyle(['width', 'height'])
    var height = fixUnit(viewStyle.height)
    var style = {}
    if (LH <= RH) {
      style = { left: 0, top: LH + 'px' }
      LH += height + span[0]
    } else if (RH < LH) {
      style = { right: 0, top: RH + 'px' }
      RH += height + span[0]
    }
    v.setStyle(style)
    // 保存LH, RH的值到state.heights
    // 当前的LH,RH其实就是下屏开始的坐标
    state.heights[index + 1] = [LH, RH]
    console.log('渲染', index, k)
  })
}

步骤五:图片加载完重新计算位置

// waterfall/index.js
Component({
  properties: {
    views: Array,
    span: {
      type: Array,
      value: [10, 10],
    },
  },
  methods: {
    onLoaded({ detail: { width, height, pIndex, index } }) {
      this.setData({
        [`views[${pIndex}][${index}].loaded`]: { width, height },
      })
    },
  },
})
function loaded(value, oldValue, ownerInstance, instance) {
  if (!value.item.loaded || !oldValue) return
	// 获取局部变量
	var state = ownerInstance.getState()

	// 判断加载是否超时,如果超时则不触发计算渲染事件
	// 让该节点保持当前的位置及高度
	var timeout = state.timeouts[value.pIndex]
	if (timeout < getDate().getTime()) {
		console.log('加载超时')
		return
	}
	
	var view = instance.selectComponent('.loaded-view')
	var viewWidth = view.getComputedStyle(['width']).width
	// 设置虚拟节点card组件里的loaded-view高度
	view.setStyle({
		height:
			computedHeight(
				viewWidth,
				value.item.loaded.width,
				value.item.loaded.height
			) + 'px',
	})
	// 加个函数防抖,因为图片加载快的情况下,会并发触发事件
	// 尽可能的少触发计算,渲染事件,保证性能
	ownerInstance.clearTimeout(timer)
	timer = ownerInstance.setTimeout(function () {
		// 渲染当前图片加载完后面的所有views
		// for循环处理当前图片所在的views,以及后面所有的views
		// 因为有些图片过大,可能会加载5s左右,但是用户如果上拉又加载了
		// 一屏内容并且也通过计算渲染了,这时候上一屏又触发了计算渲染
		// 那么可能位置信息就会发生变化,导致被遮挡,或者有空白,这时候只能
		// 计算触发事件的图片以及后面的图片,保证位置信息是正确的
		for (var i = value.pIndex; i < state.page; i++) {
			console.log('需要渲染', i)
			refreshViews(i, ownerInstance, state)
		}
	}, 300)
}

优化

瀑布流最好后台会返回图片的尺寸信息,然后初次渲染的时候就计算好节点的长宽比例,这样就不用监听图片loaded事件了,瀑布流组件代码也不会频繁触发计算渲染,性能也好,方法也简单。

<image 
	src="xxxx"
	style="{{ wxs.computed({width, height}) }}" 
/>
// wxs
function computed(option) {
	// 节点宽度自己去计算
	var viewWidth = 375 / 2
	var width = option.width
	var height = option.height
	return (viewWidth / width) * height + 'px
}

完整代码

打开代码片段https://developers.weixin.qq.com/s/SO5q6UmF7doL,可直接运行。

https://github.com/liziwork/li-ui github 如果打不开,请切换到码云,gitee.com,代码同步更新的,觉得有用动动您的小手点个Star。

扫码查看更多组件

最后一次编辑于  03-19  
点赞 0
收藏
评论

1 个评论

  • 肥喵
    肥喵
    03-19

    图片高度会抖动一下?是否是高度没有计算好?

    03-19
    赞同
    回复 1
    • 鲤子
      鲤子
      03-19
      页面初次渲染的时候,图片的节点有个默认高度,待图片加载完会触发修改样式的事件,会动态改变图片的高度,所以会有视觉差。
      03-19
      回复
登录 后发表内容