Git Product home page Git Product logo

vue-virtual-waterfall's Introduction

vue-virtual-waterfall

A Vue 3 virtual waterfall component

npm

中文文档

Example

Usage

pnpm add @lhlyu/vue-virtual-waterfall

  • Local import
import { VirtualWaterfall } from '@lhlyu/vue-virtual-waterfall'
  • Global import
import VueVirtualWaterfall from '@lhlyu/vue-virtual-waterfall'

app.use(VueVirtualWaterfall)
  • Usage
<template>
    <VirtualWaterfall :items="items" :calcItemHeight="calcItemHeight">
        <template #default="{ item }: { item: ItemOption }">
            <div class="card">
                <img :src="item.img" />
            </div>
        </template>
    </VirtualWaterfall>
</template>
  • Nuxt3 Usage

demo

Attention!!!

The VirtualWaterfall component wants to implement a virtual list, and the container that wraps it must indicate a fixed height. The scrolling event can be bound to this container. If this component is hung under the body, the height of the body also needs to be specified. The scrolling event can be bound to the window

Documentation

  • Properties
Field Type Default Description
virtual boolean true Enable virtual list
rowKey string 'id' Key for v-for
gap number 15 Gap between each item
padding number or string 15 or '15px 15px' Container's padding
preloadScreenCount [number, number] [0, 0] Preload screen count [above, below]
itemMinWidth number 220 Minimum width for each item
maxColumnCount number 10 Maximum number of columns
minColumnCount number 2 Minimum number of columns
items any[] [] Data
calcItemHeight (item: any, itemWidth: number) => number (item: any, itemWidth: number) => 250 Method to calculate item height
  • Slots
Event Type Description
default { item: any, index: number } Custom default content

vue-virtual-waterfall's People

Contributors

lhlyu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

vue-virtual-waterfall's Issues

Kind notice

On behalf of Shugetsu Soft (The current maintainer of Pixivel), we have sent you an important mail regarding the possible abuse of our services. Please check your mail at [email protected] for more details.

虚拟列表不生效

我打开开发者工具查看 dom 节点发现只有向下滚动的时候,元素节点才会减少,但也不是 demo 中的效果。
我的元素节点数量达到 30 个左右,而且向上滚动,节点数量只会增加。
于是我下载了您的源码跑来看,又是正常的,然后逐一排查,发现您的 main.ts 中 app 是挂载到 body 中的,
然后我尝试修改成挂载到 body,结果是虚拟列表生效了。

提这个 issue 主要是想建议您可以在文档中添加这个说明。
最后,感谢大佬的贡献!

使用keep-alive的时候出现bug

我在首页展示瀑布流列表,想要在跳转到下一个路由之后保持页面状态,于是使用了 keep-alive 组件,
然后我遇到了问题,在使用导航返回到首页之后,页面只展示一个item。
我的代码如下:
<keep-alive include="waterFall"> <router-view></router-view> </keep-alive>
期待大佬的答复!谢谢!

itemWidth和itemMinWidth之间语义上有冲突?

实际代码中itemMinWidth只限于计算列数,不知道是有什么特殊考虑?

目前计算出的itemWidth明显小于itemMinWidth,所以从语义上看是有冲突的。

我现在做一个响应式页面,希望用固定宽度,目前看源码是不支持的。是否方便增加这样的支持?

npm 包能否移除@arco-design/web-vue 和 mockjs

npm包能否把这两个依赖放到 devDependencies 这里,或者打npm包的时候,不要打进来。因为组件并没有使用这两个包,只是示例使用了。

"@arco-design/web-vue": "^2.50.2",
"mockjs": "^1.1.0",

局部使用该虚拟列表时,由于获取参与计算的top是基于视口的,列表元素会在全局滚动时异常响应修改,我简单修改 基于父元素的scrollTop去做运算,测不出问题,不知道楼主是不是有其他考虑?

<template>
    <div ref="content" :style="{
        position: 'relative',
        willChange: 'height',
        height: `${Math.max(...columnsTop)}px`,
        padding: `${isNumber(padding) ? padding + 'px' : padding}`
    }">
        <div v-for="data in itemRenderList" :key="data.item[rowKey] ?? data.index" :style="{
            position: 'absolute',
            contentVisibility: 'auto',
            width: `${itemWidth}px`,
            height: `${data.height}px`,
            transform: `translate(${data.left}px, ${data.top}px)`,
            containIntrinsicSize: `${itemWidth}px ${data.height}px`
        }" :data-index="data.index">
            <slot :item="data.item" :index="data.index"></slot>
        </div>
    </div>
</template>

<script setup lang="ts">
import { useElementSize } from '@vueuse/core';
import { computed, onMounted, ref, watchEffect } from 'vue';

defineOptions({
    name: 'VirtualWaterfall'
})

interface VirtualWaterfallOption {
    // 是否启用虚拟列表
    virtual?: boolean
    rowKey?: string
    // item间隔
    gap?: number
    // 容器内边距
    padding?: number | string
    // 预加载屏数量 [top, bottom]
    preloadScreenCount?: [number, number]
    // item最小宽度
    itemMinWidth?: number
    // 最大列数
    maxColumnCount?: number
    // 最小列数
    minColumnCount?: number
    // 数据
    items?: any[]
    // 计算单个item高度的方法
    calcItemHeight?: (item: any, itemWidth: number) => number
}

const props = withDefaults(defineProps<VirtualWaterfallOption>(), {
    virtual: true,
    rowKey: 'id',
    gap: 15,
    padding: 15,
    preloadScreenCount: () => [0, 0],
    itemMinWidth: 220,
    maxColumnCount: 10,
    minColumnCount: 2,
    items: () => [],
    calcItemHeight: (item: any, itemWidth: number) => 250
})

const slot = defineSlots<{
    default(props: { item: any; index: number }): any
}>()

const content = ref<HTMLDivElement>()

const { width: contentWidth } = useElementSize(content)



onMounted(() => {
    // 这里是为了解决这个问题:
    // https://github.com/lhlyu/vue-virtual-waterfall/issues/5
    if (contentWidth.value === 0) {
        contentWidth.value = Number.parseInt(window.getComputedStyle(content.value).width)
    }

    createItemRenderList()

    content.value.parentElement.addEventListener('scroll', () => {
        createItemRenderList()
    })

})

function isNumber(value) {
    return Object.prototype.toString.call(value) === '[object Number]';
}

// 计算列数
const columnCount = computed<number>(() => {
    if (!contentWidth.value) {
        return 0
    }
    const cWidth = contentWidth.value
    if (cWidth >= props.itemMinWidth * 2) {
        const count = Math.floor(cWidth / props.itemMinWidth)
        if (props.maxColumnCount && count > props.maxColumnCount) {
            return props.maxColumnCount
        }
        return count
    }
    return props.minColumnCount
})

// 每列距离顶部的距离
const columnsTop = ref(new Array(columnCount.value).fill(0))

// 计算每个item占据的宽度: (容器宽度 - 间隔) / 列数
const itemWidth = computed<number>(() => {
    if (!contentWidth.value || columnCount.value <= 0) {
        return 0
    }
    // 列之间的间隔
    const gap = (columnCount.value - 1) * props.gap

    return Math.ceil((contentWidth.value - gap) / columnCount.value)
})

interface SpaceOption {
    index: number
    item: any
    column: number
    top: number
    left: number
    bottom: number
    height: number
}

// 计算每个item占据的空间
const itemSpaces = ref<SpaceOption[]>([])

watchEffect(() => {

    if (!columnCount.value) {
        itemSpaces.value = []
        return
    }

    const length = props.items.length
    const spaces = new Array(length)

    let start = 0
    // 是否启用缓存:只有当新增元素时,只需要计算新增元素的信息
    const cache = itemSpaces.value.length && length > itemSpaces.value.length
    if (cache) {
        start = itemSpaces.value.length
    } else {
        columnsTop.value = new Array(columnCount.value).fill(0)
    }

    // 为了高性能采用for-i
    for (let i = 0; i < length; i++) {
        if (cache && i < start) {
            spaces[i] = itemSpaces.value[i]
            continue
        }

        const columnIndex = getColumnIndex()
        // 计算元素的高度
        const h = props.calcItemHeight(props.items[i], itemWidth.value)
        const top = columnsTop.value[columnIndex]
        const left = (itemWidth.value + props.gap) * columnIndex

        const space: SpaceOption = {
            index: i,
            item: props.items[i],
            column: columnIndex,
            top: top,
            left: left,
            bottom: top + h,
            height: h
        }

        // 累加当前列的高度
        columnsTop.value[columnIndex] += h + props.gap
        spaces[i] = space
    }
    itemSpaces.value = spaces

    createItemRenderList()
})

let lineStart = -1
let lineEnd = -1

const createItemRenderList = () => {
    const parent = content.value.parentElement
    const length = itemSpaces.value.length
    if (!length || !parent) {
        return []
    }
    if (!props.virtual) {
        return itemSpaces.value
    }
    // 父节点距离顶部的距离
    const tp = parent.scrollTop - parent.offsetTop

    const [topPreloadScreenCount, bottomPreloadScreenCount] = props.preloadScreenCount
    // 避免多次访问
    const innerHeight = content.value.parentElement.clientHeight

    // 顶部的范围: 向上预加载preloadScreenCount个屏幕,Y轴上部
    const minLimit = tp - topPreloadScreenCount * innerHeight
    // 底部的范围: 向下预加载preloadScreenCount个屏幕
    const maxLimit = tp + (bottomPreloadScreenCount + 1) * innerHeight

    let start = 0
    let end = 0
    let open = true


    for (let i = 0; i < length; i++) {
        const v = itemSpaces.value[i]
        const t = v.top
        const b = v.bottom
        // 这里的逻辑是:
        // 只要元素部分出现在容器里就算作可见,因此有三段判断:
        // 1. 元素的上边界在容器内
        // 2. 元素的下边界在容器内
        // 3. 元素覆盖了整个容器
        if (
            (t >= minLimit && t <= maxLimit) ||
            (b >= minLimit && b <= maxLimit) ||
            (t < minLimit && b > maxLimit)
        ) {
            if (open) {
                start = i
                open = false
            }
            end = i
        }
    }

    if (lineStart == start && lineEnd === end) return

    lineStart = start

    lineEnd = end

    // 测试发现slice方法很快
    itemRenderList.value = itemSpaces.value.slice(start, end + 1)
}

// 虚拟列表逻辑:需要渲染的items
const itemRenderList = ref([])

// 获取当前元素应该处于哪一列
const getColumnIndex = (): number => {
    return columnsTop.value.indexOf(Math.min(...columnsTop.value))
}
</script>

1.1.0版本的思考

  1. 内置真实高度计算方法
  2. 修改虚拟列表遍历算法,改成双指针判断,为了避免特殊情况(出现一个高度非常高独占一列的元素),必须针对每个元素进行判断

瀑布流元素整体是线性增加的,使用区间树算法并没有很好的解决元素多时计算可见元素的耗时问题

  1. keep-alive路由切换时,滚动条的位置回到了0点
  2. 加载了大量数据后resize窗口,会重新计算一遍所有项目的位置、高度
  3. 优化下图耗时问题

image

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.