Skip to content

Instantly share code, notes, and snippets.

@daidr
Created January 14, 2022 08:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save daidr/1b8686b4ab3fd6e5b738e483a3c817f4 to your computer and use it in GitHub Desktop.
Save daidr/1b8686b4ab3fd6e5b738e483a3c817f4 to your computer and use it in GitHub Desktop.
Masonry layout in vue3
<script setup>
import { onActivated, onDeactivated, onMounted, onBeforeUnmount, reactive, nextTick } from 'vue'
import { getCurrentInstance } from '@vue/runtime-core'
const self = getCurrentInstance()
const props = defineProps({
defaultCol: {
type: Number,
default: 4
},
breakpoints: {
type: Object,
default: function () {
return {
xs: 1,
sm: 2,
md: 3,
lg: 3,
xl: 4,
'2xl': 4
}
}
},
onFetchRequest: {
type: Function,
default: function () {}
}
})
// 节流函数
function throttle(func, wait) {
let timeout
return function () {
let context = this
let args = arguments
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
func.apply(context, args)
}, wait)
}
}
}
const data = reactive({
lastPostId: 0, // 上一次刷新的最后一个帖子id
postArray: [], // 当前加载的全部帖子列表
fetching: false, // 新数据请求锁
rendering: false, // 渲染中,防止渲染未完成高度获取不准确而导致可连续触发请求
renderList: [], // 渲染列表
col: props.defaultCol, // 瀑布流列数
isEventHandled: false
})
let resizeHandler = function () {} // 窗体大小改变事件处理函数(节流)
// 用于清空渲染列表
const clearRenderList = () => {
for (var i = 0; i < data.renderList.length; i++) {
data.renderList[i].splice(0)
}
}
// 用于清空瀑布流列表
const clearPostList = () => {
data.postArray.splice(0)
clearRenderList()
}
// 窗体大小改变事件处理函数(非节流)
const onWidthChange = () => {
// 数据获取或元素渲染过程中不会触发瀑布流重布局
if (data.fetching || data.rendering) return
// 储存旧列数
let oldCol = data.col
// 获取窗体宽度以匹配断点
let curWidth = window.innerWidth
if (curWidth < 360) {
data.col = props.breakpoints['xs']
} else if (curWidth < 640) {
data.col = props.breakpoints['sm']
} else if (curWidth < 768) {
data.col = props.breakpoints['md']
} else if (curWidth < 1024) {
data.col = props.breakpoints['lg']
} else if (curWidth < 1280) {
data.col = props.breakpoints['xl']
} else {
data.col = props.breakpoints['2xl']
}
// 若列数变更,则重新渲染所有卡片
if (oldCol != data.col) {
// 加锁防止渲染过程中新的数据请求
data.rendering = true
clearRenderList()
// 等待dom更新,填充数据
nextTick(() => {
fillData()
})
}
}
//新数据获取
const getDataList = () => {
// 当前瀑布流不处于渲染状态
if (!data.rendering) {
// 加锁,防止数据获取过程中触发渲染事件
data.fetching = true
// 调用数据获取函数,该函数应该接受上次帖子列表的最后一个帖子id用于请求数据,并返回Promise
props.onFetchRequest(data.lastPostId).then((res) => {
data.rendering = true
if (res.length > 0) {
data.lastPostId = res[res.length - 1]
// 获取帖子总数(新数据除外)
let len = data.postArray.length
// 加入新帖子数据
data.postArray.push(...res)
//仅渲染新帖子
fillData(len)
} else {
// 没有数据,暂时没有额外的处理步骤
data.rendering = false
}
data.fetching = false
})
} else {
//处于请求状态,节流,或已出现无数据(over),忽略请求
return false
}
}
// 数据填充函数,会将新卡片添加到高度最小的那一列,防止瀑布流高度差过大
const fillData = (index = 0) => {
if (index < data.postArray.length) {
// 先对帖子图片进行预加载,防止卡片高度计算不准确
let imgPreload = new Image()
imgPreload.onload = function () {
// 储存所有列的目前高度
let colHeightList = []
for (let i = 0; i < data.col; i++) {
colHeightList[i] = self.refs['col' + i].offsetHeight
}
//获取高度最小的列索引
let min = colHeightList.indexOf(Math.min.apply(Math, colHeightList))
// 将帖子卡片插入到该列
data.renderList[min].push(data.postArray[index])
// 等待dom更新再插入下一条帖子
nextTick(() => {
fillData(index + 1)
})
}
imgPreload.src = data.postArray[index].image
} else {
data.rendering = false
}
}
onMounted(() => {
//初始化瀑布流渲染列表
for (var i = 0; i < data.col; i++) {
data.renderList[i] = []
}
// 将事件处理做节流包装
resizeHandler = throttle(onWidthChange, 200)
// 窗体大小改变事件监听
window.addEventListener('resize', resizeHandler)
data.isEventHandled = true
// 初始化瀑布流列数
onWidthChange()
nextTick(() => {
getDataList()
fillData(0)
})
})
onBeforeUnmount(() => {
// 取消窗体大小改变事件监听
window.removeEventListener('resize', resizeHandler)
data.isEventHandled = false
})
onDeactivated(() => {
// 取消窗体大小改变事件监听
window.removeEventListener('resize', resizeHandler)
data.isEventHandled = false
})
onActivated(() => {
if (!data.isEventHandled) {
window.addEventListener('resize', resizeHandler)
resizeHandler()
data.isEventHandled = true
}
})
defineExpose({
clearPostList,
getDataList
})
</script>
<template>
<div v-show="data.postArray" ref="masonryContainer" class="masonry-container">
<ul class="flex justify-around items-start gap-x-3 md:gap-x-5">
<!--列的宽度应由计算的来-->
<li
v-for="(it, index) in data.col"
:ref="'col' + index"
:key="it"
class="gap-y-3 flex flex-col w-full md:gap-y-5"
>
<div v-for="item in data.renderList[index]" :key="item.id">
<slot :item-data="item" />
</div>
</li>
</ul>
<button v-if="!data.rendering" :disabled="data.fetching" class="fetch-btn" @click="getDataList">
<span>加载更多</span>
<svg xmlns="http://www.w3.org/2000/svg" height="38" width="38" viewBox="0 0 120 120">
<circle cx="50%" cy="50%" r="50" fill="none" class="stroke-primary-extralight stroke-15" />
</svg>
</button>
</div>
</template>
<style lang="scss" scoped>
@keyframes loading {
0% {
stroke-dasharray: 314, 314;
stroke-dashoffset: 314;
}
50% {
stroke-dasharray: 314, 314;
stroke-dashoffset: 100;
}
100% {
stroke-dasharray: 314, 314;
stroke-dashoffset: -314;
}
}
@keyframes loading-rotate {
0% {
transform: rotateZ(0deg);
}
100% {
transform: rotateZ(360deg);
}
}
.masonry-container {
@apply w-full h-full flex flex-col items-center;
ul {
@apply px-2;
}
.fetch-btn {
@apply w-55 h-12 transition-all my-10 bg-primary-extralight text-lg text-primary border-primary border-2 py-2 px-18 rounded-full relative duration-300;
span {
@apply absolute top-1/2 left-1/2 transform-gpu -translate-x-1/2 -translate-y-1/2 opacity-100 transition-all;
}
svg {
@apply absolute top-1/2 left-1/2 transform-gpu -translate-x-1/2 -translate-y-1/2 opacity-0 transition-all stroke-cap-round;
}
}
.fetch-btn:hover {
@apply bg-primary text-primary-extralight;
}
.fetch-btn:disabled {
@apply bg-primary border-primary cursor-default w-12 p-0;
span {
@apply opacity-0;
}
svg {
@apply opacity-100;
animation: loading 2s infinite;
circle {
@apply origin-center;
animation: loading-rotate 2s infinite;
}
}
}
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment