Vant PullRefresh 下拉刷新组件完全指南:从基础用法到源码级原理解析
【免费下载链接】vantA lightweight, customizable Vue UI library for mobile web apps.项目地址: https://gitcode.com/GitHub_Trending/va/vant
导读
PullRefresh是 Vant 移动端组件库中用于提供「下拉刷新」交互的核心组件,广泛用于列表页、内容流页面的数据手动刷新场景。本文以 pull-refresh 组件文档 为主体,结合组件源码、样式定义与单元测试,系统讲解其引入方式、基础用法、成功提示、自定义插槽、完整 API 参考与主题定制方案,并深入剖析其「顶部检测—阻尼回弹—状态机驱动」的底层实现原理,帮助你在项目中快速落地并深入理解这一交互组件。
组件简介与引入方式
PullRefresh通过「手指在页面顶部下拉—释放触发刷新—加载完成回弹」的完整手势链路,为移动端列表提供刷新交互。它的实现文件位于 PullRefresh.tsx,样式位于 index.less,并在 index.ts 中通过withInstall包装后对外导出,同时注册了全局组件VanPullRefresh。
推荐通过app.use进行全局注册:
import { createApp } from 'vue'; import { PullRefresh } from 'vant'; const app = createApp(); app.use(PullRefresh);更多注册方式(局部注册、按需引入等)可参考 组件注册。
基础用法:v-model 与 refresh 事件
下拉刷新时组件会触发refresh事件。在事件的回调函数中可以执行同步或异步的数据请求操作,操作完成后需要将v-model设置为false,表示加载完成,组件才会收起头部并恢复初始状态。
<van-pull-refresh v-model="loading" @refresh="onRefresh"> <p>刷新次数: {{ count }}</p> </van-pull-refresh>import { ref } from 'vue'; import { showToast } from 'vant'; export default { setup() { const count = ref(0); const loading = ref(false); const onRefresh = () => { setTimeout(() => { showToast('刷新成功'); loading.value = false; count.value++; }, 1000); }; return { count, loading, onRefresh, }; }, };需要注意两点:
v-model绑定的loading是受控状态:组件触发refresh后会将modelValue置为true,业务侧必须手动改回false才能结束加载动画。从源码看,onTouchEnd中会先emit('update:modelValue', true),再通过nextTick派发refresh事件,确保v-model值变化能被业务侧的watch及时感知(见 PullRefresh.tsx)。refresh事件没有任何回调参数,刷新后的数据获取逻辑完全由业务侧自行编排。
官方演示页 demo/index.vue 中还展示了一个细节:刷新过程中调用showToast('刷新成功')与通过success-text展示成功提示是两种互斥的反馈方式,实际项目中可以按交互规范二选一。
成功提示:success-text 与 success-duration
通过success-text可以设置刷新成功后的顶部提示文案。当v-model被业务侧置为false时,组件会先展示「成功」状态,停留success-duration毫秒后再收起头部。
<van-pull-refresh v-model="isLoading" success-text="刷新成功" @refresh="onRefresh" > <p>刷新次数: {{ count }}</p> </van-pull-refresh>该流程对应源码中的showSuccessTip方法:设置status = 'success',并在successDuration毫秒后通过setStatus(0)复位(见 PullRefresh.tsx)。success-duration默认值为500(毫秒),可传入字符串形式的数字,例如success-duration="800"。
需要说明的是:只有配置了success-text或success插槽时,成功提示阶段才会出现;否则v-model置为false后组件直接回到normal状态。
自定义提示:五个状态插槽
通过插槽可以完全自定义下拉刷新过程中的提示内容。组件共暴露 5 个头部状态插槽:normal、pulling、loosing、loading、success,其中pulling、loosing、loading会接收{ distance }插槽参数,可用于实现随下拉距离缩放的动画效果。
<van-pull-refresh v-model="isLoading" :head-height="80" @refresh="onRefresh"> <!-- 下拉提示,通过 scale 实现一个缩放效果 --> <template #pulling="{ distance }"> <img class="doge" src="https://fastly.jsdelivr.net/npm/@vant/assets/doge.png" :style="{ transform: `scale(${distance / 80})` }" /> </template> <!-- 释放提示 --> <template #loosing> <img class="doge" src="https://fastly.jsdelivr.net/npm/@vant/assets/doge.png" /> </template> <!-- 加载提示 --> <template #loading> <img class="doge" src="https://fastly.jsdelivr.net/npm/@vant/assets/doge-fire.jpeg" /> </template> <p>刷新次数: {{ count }}</p> </van-pull-refresh> <style> .doge { width: 140px; height: 72px; margin-top: 8px; border-radius: 4px; } </style>官方 Demo 中通过cdnURL('doge.png')预加载图片并基于distance / headHeight计算缩放比例(见 demo/index.vue)。实际项目中应替换为业务自己的图片资源,并建议同样在onMounted阶段预加载,避免下拉时图片闪烁。
在渲染逻辑上(renderStatus,见 PullRefresh.tsx),优先级为:插槽 > 内置文案。即某个状态下定义了插槽,就渲染插槽内容;未定义插槽时,pulling/loosing/success渲染纯文本,loading渲染 Loading 组件 加加载文案。
状态机与触发原理:源码级解析
从源码结构看,PullRefresh的交互本质是一个五状态机,由normal → pulling → loosing → loading → success流转(见 PullRefresh.tsx):
| 状态 | 含义 | 进入条件 |
|---|---|---|
normal | 静止状态 | distance === 0 |
pulling | 正在下拉 | 0 < distance < pullDistance |
loosing | 已超过阈值、可释放 | distance >= pullDistance |
loading | 刷新加载中 | 在loosing状态松手 |
success | 成功提示 | 加载完成后且配置了成功提示 |
状态由setStatus方法统一驱动,并同步派发change事件(见 PullRefresh.tsx):
const setStatus = (distance: number, isLoading?: boolean) => { const pullDistance = +(props.pullDistance || props.headHeight); state.distance = distance; if (isLoading) { state.status = 'loading'; } else if (distance === 0) { state.status = 'normal'; } else if (distance < pullDistance) { state.status = 'pulling'; } else { state.status = 'loosing'; } emit('change', { status: state.status, distance }); };触发条件:父级滚动容器在顶部
PullRefresh的核心触发条件是「父级滚动元素的滚动条在顶部位置」。checkPosition通过getScrollTop(scrollParent.value!) === 0判断是否到达顶部(见 PullRefresh.tsx),只有到达顶部时才记录触摸起点并允许下拉。滚动容器由useScrollParent(root)自动向上查找确定,它可能是window,也可能是最近的overflow: auto/scroll元素。
阻尼回弹算法
为了让下拉超过阈值后手感更「有阻力」,源码中实现了分段阻尼算法ease(见 PullRefresh.tsx):
const ease = (distance: number) => { const pullDistance = +(props.pullDistance || props.headHeight); if (distance > pullDistance) { if (distance < pullDistance * 2) { distance = pullDistance + (distance - pullDistance) / 2; } else { distance = pullDistance * 1.5 + (distance - pullDistance * 2) / 4; } } return Math.round(distance); };从代码可推断出其阻尼策略:
- 手指位移 ≤
pullDistance时,头部跟随手指 1:1 移动; - 位移在
pullDistance与2 × pullDistance之间时,超出部分按1/2衰减; - 位移超过
2 × pullDistance后,再超出部分按1/4衰减,并存在1.5 × pullDistance的封顶参考值。
这一设计保证了即便用户暴力猛拉,头部位移也不会无限增大。
手势与方向判定
- 触摸方向判定复用
useTouch组合式函数(见 use-touch.ts),通过deltaX与deltaY的比较锁定滑动方向:只有垂直下滑(isVertical()且deltaY >= 0)才会触发下拉逻辑,横向滑动会被忽略,避免与页面横向滚动冲突。 touchmove监听通过useEventListener挂载在track元素上,并将passive设为false以消除 Chrome 的被动监听警告,从而允许调用preventDefault阻止页面原生回弹(见 PullRefresh.tsx)。- 在
normal、loading、success状态或disabled为真时,组件会忽略触摸操作(isTouchable,见 PullRefresh.tsx),加载中重复下拉不会造成状态错乱。
位移与动画
- 头部位移通过
translate3d(0, distance, 0)施加在track元素上,动画时长由animation-duration(默认300ms)控制(见 PullRefresh.tsx)。 - 头部区域默认通过
transform: translateY(-100%)隐藏在内容上方,仅在手指下拉时被「拉出」(见 index.less)。
API 完整参考
Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| v-model | 是否处于加载中状态 | boolean | - |
| pulling-text | 下拉过程提示文案 | string | 下拉即可刷新... |
| loosing-text | 释放过程提示文案 | string | 释放即可刷新... |
| loading-text | 加载过程提示文案 | string | 加载中... |
| success-text | 刷新成功提示文案 | string | - |
| success-duration | 刷新成功提示展示时长(ms) | number | string | 500 |
| animation-duration | 动画时长 | number | string | 300 |
| head-height | 顶部内容高度 | number | string | 50 |
| pull-distance | 触发下拉刷新的距离 | number | string | 与head-height一致 |
| disabled | 是否禁用下拉刷新 | boolean | false |
补充说明(依据 pullRefreshProps):
pulling-text、loosing-text的默认文案来自国际化语言包vanPullRefresh配置(见 zh-CN.ts),会随组件库语言切换自动变化;loading-text的默认值加载中...则由组件内置兜底。head-height与pull-distance的关系:pull-distance不传时,触发距离与head-height一致。二者均通过makeNumericProp/numericProp声明,支持数字或字符串(如"80")写法,内部以+props.pullDistance转为数字比较。- 当
head-height不等于默认的50时,组件会为头部元素显式设置行内height(见 PullRefresh.tsx),测试用例should set height when using head-height对此进行了验证(见 test/index.spec.ts)。
Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| refresh | 下拉刷新时触发 | - |
| change | 拖动时或状态改变时触发 | { status: string, distance: number } |
change事件在手势拖动与状态切换的每个关键节点都会触发,可用于埋点或联动外部 UI。测试用例should emit change event when status changed验证了其载荷结构,例如[{ distance: 20, status: 'pulling' }](见 test/index.spec.ts)。
Slots
| 名称 | 说明 | 参数 |
|---|---|---|
| default | 自定义内容(列表主体) | - |
| normal | 非下拉状态时顶部内容 | - |
| pulling | 下拉过程中顶部内容 | { distance: number } |
| loosing | 释放过程中顶部内容 | { distance: number } |
| loading | 加载过程中顶部内容 | { distance: number } |
| success | 刷新成功提示内容 | - |
类型定义
组件导出以下类型定义,便于在 TypeScript 项目中获得完整的类型提示:
import type { PullRefreshProps } from 'vant';相关类型在 PullRefresh.tsx 中通过ExtractPropTypes从pullRefreshProps推导,并通过 index.ts 对外导出。
主题定制:CSS 变量
组件提供下列 CSS 变量用于自定义样式,使用方式可参考 ConfigProvider 组件。变量在 index.less 的:root/:host中声明,对应的 TypeScript 主题变量类型定义见 types.ts。
| 名称 | 默认值 | 描述 |
|---|---|---|
| --van-pull-refresh-head-height | 50px | 头部高度 |
| --van-pull-refresh-head-font-size | var(--van-font-size-md) | 头部文字字号 |
| --van-pull-refresh-head-text-color | var(--van-text-color-2) | 头部文字颜色 |
| --van-pull-refresh-loading-icon-size | 16px | 加载图标尺寸 |
例如在页面级覆盖头部高度与文字颜色:
.demo-page { --van-pull-refresh-head-height: 60px; --van-pull-refresh-head-text-color: #1989fa; }常见问题
内容未填满屏幕时,只有一部分区域可以下拉?
默认情况下,下拉区域的高度与内容高度保持一致。若希望下拉区域始终为全屏,可以给PullRefresh设置一个与屏幕大小相等的最小高度:
<van-pull-refresh style="min-height: 100vh;" />PullRefresh 的触发条件是?
触发条件是「父级滚动元素的滚动条在顶部位置」:
- 如果最近一个可滚动的父级元素是
window,则要求window.pageYOffset === 0; - 如果最近一个可滚动的父级元素是
Element,则要求Element.scrollTop === 0。
这一点在源码checkPosition中通过getScrollTop(scrollParent.value!) === 0判断,并有对应的单元测试覆盖:mockScrollTop(1)时下拉不会触发update:modelValue,回到顶部后才触发(见 test/index.spec.ts)。
在桌面端无法操作组件?
PullRefresh依赖触摸事件(touchstart/touchmove/touchend),桌面端浏览器默认不支持触屏手势,需配合桌面端适配方案使用,参见 桌面端适配。
下拉距离小于阈值松手会怎样?
未达到pull-distance时松手,组件直接回弹复位(setStatus(0)),不会触发refresh事件,也不会更新v-model。对应测试用例should not emit update:modelValue event after pulling a short distance(见 test/index.spec.ts),这保证了误触下拉不会造成无谓的接口请求。
小结
PullRefresh是 Vant 中一个「小巧但完整」的交互组件:对外提供简洁的v-model+refresh事件协议、五个可自定义的头部状态插槽与完整的主题变量;对内则以五状态机为骨架,配合顶部位置检测、方向锁定、分段阻尼算法,构建了接近原生 App 的刷新手感。阅读其 源码 与 测试用例,也能为自研类似「手势拖拽 + 状态机」组件提供一份高质量的参考范本。
【免费下载链接】vantA lightweight, customizable Vue UI library for mobile web apps.项目地址: https://gitcode.com/GitHub_Trending/va/vant
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考