List
List 列表 瀑布流滚动加载,用于展示长列表,当列表即将滚动到底部时,会触发事件并加载更多列表项。
组件注册
ts
import { createApp } from 'vue';
import { List } from 'vant';
const app = createApp();
app.use(List);
使用
vue
<template>
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell v-for="item in list" :key="item" :title="item" />
</van-list>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const onLoad = () => {
// 异步更新数据
// setTimeout 仅做示例,真实场景中一般为 ajax 请求
setTimeout(() => {
for (let i = 0; i < 10; i++) {
list.value.push(list.value.length + 1);
}
// 加载状态结束
loading.value = false;
// 数据全部加载完成
if (list.value.length >= 40) {
finished.value = true;
}
}, 1000);
};
return {
list,
onLoad,
loading,
finished,
};
},
};
</script>
组件源码
tsx
import {
ref,
watch,
nextTick,
onUpdated,
onMounted,
defineComponent,
type ExtractPropTypes,
} from 'vue';
// Utils
import {
isHidden,
truthProp,
makeStringProp,
makeNumericProp,
createNamespace,
} from '../utils';
// Composables
import { useRect, useScrollParent, useEventListener } from '@vant/use';
import { useExpose } from '../composables/use-expose';
import { useTabStatus } from '../composables/use-tab-status';
// Components
import { Loading } from '../loading';
// Types
import type { ListExpose, ListDirection } from './types';
const [name, bem, t] = createNamespace('list');
export const listProps = {
error: Boolean,
offset: makeNumericProp(300),
loading: Boolean,
disabled: Boolean,
finished: Boolean,
errorText: String,
direction: makeStringProp<ListDirection>('down'),
loadingText: String,
finishedText: String,
immediateCheck: truthProp,
};
export type ListProps = ExtractPropTypes<typeof listProps>;
export default defineComponent({
name,
props: listProps,
emits: ['load', 'update:error', 'update:loading'],
setup(props, { emit, slots }) {
// use sync innerLoading state to avoid repeated loading in some edge cases
const loading = ref(props.loading);
const root = ref<HTMLElement>();
const placeholder = ref<HTMLElement>();
const tabStatus = useTabStatus();
// 获取最近滚动的父容器节点
const scrollParent = useScrollParent(root);
// 核心函数
const check = () => {
nextTick(() => {
if (
loading.value ||
props.finished ||
props.disabled ||
props.error ||
// skip check when inside an inactive tab
tabStatus?.value === false
) {
return;
}
const { offset, direction } = props;
// 获取滚动元素的 BoundingClientRect 坐标
const scrollParentRect = useRect(scrollParent);
// height 不存在或者根元素或父元素隐藏
if (!scrollParentRect.height || isHidden(root)) {
return;
}
// 是否到达终点
let isReachEdge = false;
// 占位符的 BoundingClientRect 坐标
const placeholderRect = useRect(placeholder);
if (direction === 'up') {
// 触顶 滚动元素距离顶部的位置 - 占位符距离顶部的位置 <= 限制值
isReachEdge = scrollParentRect.top - placeholderRect.top <= offset;
} else {
// 触底 占位符距离底部的位置 - 滚动元素距离底部的位置 <= 限制值
isReachEdge =
placeholderRect.bottom - scrollParentRect.bottom <= offset;
}
if (isReachEdge) {
// 达到终点,触发加载
loading.value = true;
emit('update:loading', true);
emit('load');
}
});
};
// 完成后的提示
const renderFinishedText = () => {
if (props.finished) {
const text = slots.finished ? slots.finished() : props.finishedText;
if (text) {
return <div class={bem('finished-text')}>{text}</div>;
}
}
};
// 点击重新发起 load 事件
const clickErrorText = () => {
emit('update:error', false);
check();
};
// 出错的提示
const renderErrorText = () => {
if (props.error) {
const text = slots.error ? slots.error() : props.errorText;
if (text) {
return (
<div
role="button"
class={bem('error-text')}
tabindex={0}
onClick={clickErrorText}
>
{text}
</div>
);
}
}
};
// 展示加载状态
const renderLoading = () => {
if (loading.value && !props.finished && !props.disabled) {
return (
<div class={bem('loading')}>
{slots.loading ? (
slots.loading()
) : (
<Loading class={bem('loading-icon')}>
{props.loadingText || t('loading')}
</Loading>
)}
</div>
);
}
};
// 状态发生改变,重新触发检查
watch(() => [props.loading, props.finished, props.error], check);
// 当顶层有父组件是 tab 标签页的时候,会通过 provide 注入 tab 的状态,发生改变重新触发检查
if (tabStatus) {
watch(tabStatus, (tabActive) => {
if (tabActive) {
check();
}
});
}
onUpdated(() => {
// 组件 DOM 树更新完毕后
loading.value = props.loading!;
});
onMounted(() => {
// 是否在初始化时立即执行滚动位置检查
if (props.immediateCheck) {
check();
}
});
// 向外部抛出 check 函数
useExpose<ListExpose>({ check });
// 滚动事件
useEventListener('scroll', check, {
target: scrollParent,
passive: true,
});
return () => {
const Content = slots.default?.();
const Placeholder = <div ref={placeholder} class={bem('placeholder')} />;
return (
<div ref={root} role="feed" class={bem()} aria-busy={loading.value}>
{props.direction === 'down' ? Content : Placeholder}
{renderLoading()}
{renderFinishedText()}
{renderErrorText()}
{props.direction === 'up' ? Content : Placeholder}
</div>
);
};
},
});