Vant PullRefresh 下拉刷新组件深度指南:从基础用法到源码级原理解析
2026/9/13 1:04:45 网站建设 项目流程

Vant PullRefresh 下拉刷新组件深度指南:从基础用法到源码级原理解析

【免费下载链接】vantA lightweight, customizable Vue UI library for mobile web apps.项目地址: https://gitcode.com/GitHub_Trending/va/vant

下拉刷新是移动端列表页最常用的交互之一。Vant 的PullRefresh组件(van-pull-refresh)以轻量、可定制著称,既支持开箱即用的默认文案与加载动画,也允许通过插槽完全自定义下拉、释放、加载与成功四个阶段的视觉表现。本文将基于 PullRefresh 官方文档,结合 PullRefresh.tsx、index.less 与 测试用例 等仓库源码,带你掌握从接入、配置、定制到原理验证的完整链路。

组件介绍与引入

PullRefresh用于提供下拉刷新的交互操作:用户下拉列表顶部,组件进入"下拉中 → 释放中 → 加载中 → 刷新成功"的状态流转,开发者在refresh事件回调中执行数据请求,完成后将v-model置回false即完成一轮刷新。

组件支持全局注册与按需引入两种方式。全局注册示例如下:

import { createApp } from 'vue'; import { PullRefresh } from 'vant'; const app = createApp(); app.use(PullRefresh);

注册后即可在模板中以<van-pull-refresh>标签使用。组件内部通过withInstall包装导出,并在 index.ts 中声明了VanPullRefresh的全局组件类型,因此 TypeScript 项目无需额外声明即可获得模板类型提示。更多注册方式可参考组件注册。

基础用法: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双向绑定的含义值得特别注意:它不是"是否允许下拉"的开关,而是"是否处于加载中"的状态标记。从源码看,组件只声明了modelValueprop 并对外触发update:modelValuerefresh两个事件:

emits: ['change', 'refresh', 'update:modelValue'],

在 onTouchEnd 中,当手指释放且状态为loosing时,组件会先把modelValue置为true(进入 loading),再在nextTick后触发refresh,确保业务代码拿到loading === true时回调已就绪:

if (state.status === 'loosing') { setStatus(+props.headHeight, true); emit('update:modelValue', true); // ensure value change can be watched nextTick(() => emit('refresh')); }

而 watch modelValue 监听loadingtrue变回false,此时如果有success插槽或success-text则展示成功提示,否则直接回弹到normal状态。这套约定让业务代码只需要关心"请求完成把 loading 置 false",其余回弹动画全部由组件接管。

成功提示:success-text 与 success-duration

通过success-text可以设置刷新成功后的顶部提示文案:

<van-pull-refresh v-model="isLoading" success-text="刷新成功" @refresh="onRefresh" > <p>刷新次数: {{ count }}</p> </van-pull-refresh>

成功提示的展示时长由success-duration(默认500ms)控制。其底层逻辑在 showSuccessTip 中:将状态置为success,并在successDuration毫秒后调用setStatus(0)将距离归零、状态恢复normal

const showSuccessTip = () => { state.status = 'success'; setTimeout(() => { setStatus(0); }, +props.successDuration); };

值得注意的是,成功提示只有在slots.successprops.successText存在时才会触发(见watch中的分支判断),这保证了不需要提示的场景下加载结束会直接利落回弹。

自定义提示:状态插槽全解析

通过插槽可以自定义下拉刷新过程中的提示内容,这是PullRefresh最有可玩性的部分。官方示例用一张柴犬图片(doge)配合scale变换实现"越拉越大"的弹性效果:

<van-pull-refresh v-model="isLoading" :head-height="80" @refresh="onRefresh"> <!-- 下拉提示,通过 scale 实现一个缩放效果 --> <template #pulling="props"> <img class="doge" src="https://fastly.jsdelivr.net/npm/@vant/assets/doge.png" :style="{ transform: `scale(${props.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>

几个关键点:

  • pulling插槽接收{ distance }参数(当前下拉距离),distance / headHeight即当前下拉进度,可用于驱动图片缩放、透明度、旋转等任意 CSS 变换;
  • loosingloading插槽同样接收{ distance }
  • 示例中将head-height提升到80,使提示区域有更大的展示空间;
  • 官方 demo/index.vue 中还通过cdnURL('doge.png')预加载了图片,避免下拉过程中图片首次加载卡顿。

插槽的渲染逻辑在 renderStatus:组件会优先查找与当前状态同名的插槽(slots[status]),找到则渲染插槽内容并注入distance;未提供插槽时才回退到默认的文案 +Loading加载动画。

插槽优先级与内置回退

如果你只自定义了部分插槽,其余状态会使用组件内置的默认展示:pulling/loosing/success显示对应文案(pulling默认"下拉即可刷新...",loosing默认"释放即可刷新..."),loading显示文案外加一个Loading旋转图标,图标大小受--van-pull-refresh-loading-icon-size控制(见 index.less)。默认文案由 locale/lang/zh-CN.ts 中的vanPullRefresh字段提供,这意味着它会跟随Locale语言包自动切换。

核心机制:状态机与源码级交互原理

要真正用好PullRefresh,理解它的五态状态机很有帮助。从 PullRefresh.tsx 可以看到组件的状态集合:

type PullRefreshStatus = | 'normal' // 初始状态 | 'pulling' // 下拉中(距离 < pullDistance) | 'loosing' // 释放中(距离 >= pullDistance) | 'loading' // 加载中 | 'success'; // 刷新成功

状态流转规则

setStatus 是唯一的状态写入入口,其判定逻辑为:

  • 明确传入isLoading时直接进入loading
  • 距离归零进入normal
  • 距离小于触发阈值pullDistance(默认与headHeight一致,即 50px)进入pulling
  • 距离达到或超过阈值进入loosing

每次状态变化都会触发change事件(携带{ status, distance }),这也是文档中change事件"拖动时或状态改变时触发"的由来。

阻尼回弹算法:ease

移动端下拉往往需要"越拉越费力"的阻尼手感,这在 ease 函数中实现:

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); };

即:手指拖拽超过触发距离后,额外拉动部分按 1/2 折算;超过两倍触发距离后进一步降为 1/4 折算。这保证无论用户用多大力下拉,head区域的位移都有上限收敛,视觉上始终处于可控范围。

触发前置条件:必须在滚动容器顶部

下拉刷新的触发并非无条件。组件通过useScrollParent找到最近的滚动父级,并在 checkPosition 中校验其滚动位置:

const checkPosition = (event: TouchEvent) => { reachTop = getScrollTop(scrollParent.value!) === 0; if (reachTop) { state.duration = 0; touch.start(event); } };

getScrollTop 统一处理了ElementscrollTopwindowpageYOffset两种取值,并通过Math.max(top, 0)抹平了 iOS 橡皮筋回弹导致的负值。随后在touchmove中,只有reachTop === truedeltaY >= 0(向下拖动)且touch.isVertical()(纵向拖动)三个条件同时满足时,才会preventDefault并更新下拉距离。方向锁定逻辑在 use-touch.ts:位移超过 10px 后锁定方向,避免横向滚动列表时误触下拉。

另外组件通过useEventListener('touchmove', onTouchMove, { target: track })监听 touchmove,并将事件监听器设为非 passive(源码注释说明是为了消除 Chrome 的警告),从而能够成功调用preventDefault阻止页面原生滚动。

API 详解

Props

参数说明类型默认值
v-model是否处于加载中状态boolean-
pulling-text下拉过程提示文案string下拉即可刷新...
loosing-text释放过程提示文案string释放即可刷新...
loading-text加载过程提示文案string加载中...
success-text刷新成功提示文案string-
success-duration刷新成功提示展示时长(ms)number | string500
animation-duration动画时长number | string300
head-height顶部内容高度number | string50
pull-distance触发下拉刷新的距离number | stringhead-height一致
disabled是否禁用下拉刷新booleanfalse

以上参数在 pullRefreshProps 中均有对应声明,其中数值型参数均支持number | string两种写法(组件内部通过+一元运算符转为数字)。几个参数的源码级说明:

  • head-height:顶部提示区域的高度,默认50。自定义插槽时通常需要调大,如示例中的80。设置后组件会为head元素显式设置height: ${headHeight}px(见 getHeadStyle)。
  • pull-distance:触发loosing → loading的位移阈值。不传时回退为headHeight,因此默认"拉满头部高度即触发"。
  • success-duration / animation-duration:分别控制成功提示停留时长与回弹过渡时长,单位均为毫秒,回弹动画通过transitionDuration施加在track元素上(见 PullRefresh.tsx)。

Events

事件名说明回调参数
refresh下拉刷新时触发-
change拖动时或状态改变时触发{ status: string, distance: number }

change事件的参数由setStatus统一发出,status取值即上文五态之一。测试用例 index.spec.ts 验证了拖动 20px 时恰好发出[{ distance: 20, status: 'pulling' }]

Slots

名称说明参数
default自定义内容-
normal非下拉状态时顶部内容-
pulling下拉过程中顶部内容{ distance: number }
loosing释放过程中顶部内容{ distance: number }
loading加载过程中顶部内容{ distance: number }
success刷新成功提示内容-

default外的五个插槽分别对应状态机的五种状态,插槽优先级高于默认文案展示(如"自定义提示"一节所述)。

类型定义

组件导出以下类型定义:

import type { PullRefreshProps } from 'vant';

对应的PullRefreshPropsExtractPropTypes<typeof pullRefreshProps>推导而来(见 PullRefresh.tsx),主题变量类型PullRefreshThemeVars则定义在 types.ts,可通过import type { PullRefreshThemeVars } from 'vant'引入。

主题定制

样式变量

组件提供了下列 CSS 变量,可用于自定义样式,使用方法可参考 ConfigProvider 组件。

名称默认值描述
--van-pull-refresh-head-height50px顶部提示区域高度
--van-pull-refresh-head-font-sizevar(--van-font-size-md)提示文案字号
--van-pull-refresh-head-text-colorvar(--van-text-color-2)提示文案颜色
--van-pull-refresh-loading-icon-size16px加载图标尺寸

这些变量在 index.less 中以:root, :host作用域声明默认值,其中--van-pull-refresh-head-heighthead-heightprop 相互对应——不传 prop 时头部高度即由该 CSS 变量决定。使用ConfigProvider包裹并覆盖这些变量,即可实现运行时主题切换。

常见问题

PullRefresh 的内容未填满屏幕时,只有一部分区域可以下拉?

默认情况下,下拉区域的高度是和内容高度保持一致的,如果需要让下拉区域始终为全屏,可以给 PullRefresh 设置一个与屏幕大小相等的最小高度:

<van-pull-refresh style="min-height: 100vh;" />

PullRefresh 的触发条件是?

PullRefresh 的触发条件是「父级滚动元素的滚动条在顶部位置」。

  • 如果最近一个可滚动的父级元素是window,则要求window.pageYOffset === 0
  • 如果最近一个可滚动的父级元素是Element,则要求Element.scrollTop === 0

这一条件在源码的checkPosition中通过getScrollTop(scrollParent) === 0统一判定,与上述两种情况一一对应。测试用例 index.spec.ts 也验证了这一点:当模拟scrollTop = 1(未到顶部)时拖拽不会触发update:modelValue,回到顶部后即可正常触发。

在桌面端无法操作组件?

PullRefresh基于触摸事件(touchstart/touchmove/touchend)实现,桌面端浏览器默认不支持触屏事件,因此需要做桌面端适配,具体方案参见桌面端适配。

测试与行为验证

仓库为PullRefresh提供了完整的行为测试,可作为理解组件契约的补充材料(见 test/index.spec.ts),关键验证点包括:

  • 状态渲染:依次模拟touchstart → touchmove(20px) → touchmove(100px) → touchend,快照断言pullingloosingloading各阶段头部内容,并断言update:modelValuerefresh事件均被触发;
  • 短距离不触发:仅拖动 10px(小于默认 50px 阈值)时update:modelValue不触发;
  • 顶部判定:非顶部时不响应下拉(上文已述);
  • 成功提示success-textsuccess插槽在modelValue置回false后正确渲染;
  • head-height 与 pull-distance:验证headHeight: 100时头部高度为100px,以及自定义pullDistance: 300时状态流转正常。

小结

PullRefresh是 Vant 中"小而美"的典型组件:对外仅暴露 10 个 prop、2 个事件与 6 个插槽,却完整覆盖了移动端下拉刷新的全部交互细节——滚动顶部判定、方向锁定、阻尼回弹、五态状态机、成功提示计时与 CSS 变量主题化。结合 PullRefresh.tsx 源码理解其状态流转与阻尼算法后,无论是做基础列表刷新、全屏下拉还是高度定制的品牌化刷新动效,都能游刃有余。

【免费下载链接】vantA lightweight, customizable Vue UI library for mobile web apps.项目地址: https://gitcode.com/GitHub_Trending/va/vant

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询