news 2026/10/6 15:59:38

VueUse useSortable 实战指南:在 Vue 3 中轻松实现列表拖拽排序

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VueUse useSortable 实战指南:在 Vue 3 中轻松实现列表拖拽排序
  • 前端

【免费下载链接】vueuse

Collection of essential Vue Composition Utilities for Vue 3

项目地址:https://gitcode.com/gh_mirrors/vu/vueuse
点击查看免费下载

导读

useSortable是 VueUse@vueuse/integrations包中针对 SortableJS 的响应式封装,它让开发者无需手动管理 Sortable 实例的生命周期,即可在 Vue 3 组件中快速实现列表的拖拽排序,同时保持拖拽结果与响应式数据源的双向同步。阅读本文后,你将掌握useSortable的安装、四种主流使用姿势(模板引用、CSS 选择器、组件模式)、返回方法(start/stop/option)的运行时用法、watchElement自动重初始化策略,以及如何通过自定义onUpdate处理器精细控制数据变更逻辑。

是什么:一个薄而完整的 SortableJS 响应式封装

useSortable定位简单明确:它是 SortableJS 的包装器(Wrapper)。它并不重新发明拖拽算法,而是把 SortableJS 的实例创建、销毁、选项读取与修改,以及拖拽结束后的数组重排,全部收敛为符合 VueUse 风格的可组合函数,让使用者拿到的是干净的返回值接口。

从源码结构看(packages/integrations/useSortable/index.ts),整个模块只依赖三样东西:

  • sortablejs作为底层拖拽引擎;
  • VueUse 自身的工具函数(tryOnMounted、tryOnScopeDispose、unrefElement、defaultDocument等)负责生命周期与元素解析;
  • Vue 的响应式 API(isRef、nextTick、toValue、watch)负责数据同步。

它在 packages/integrations/index.ts 中被统一导出,属于 VueUse 的「集成层」(@vueuse/integrations),同层还有useAxios、useDrauu、useQRCode等一批第三方库封装。官方文档(packages/integrations/README.md)建议为了更好的 tree-shaking 效果,优先从子模块路径导入,例如import { useSortable } from '@vueuse/integrations/useSortable',而非从主入口@vueuse/integrations整体导入。

需要特别留意的一点限制:目前useSortable只支持单列表内部的拖拽排序,跨列表拖拽(多列表之间的移动)尚未实现,规划功能时应预先评估该边界。

安装:两个依赖缺一不可

useSortable是一个可选集成,SortableJS 被声明为peerDependenciesMeta中的可选依赖(见 packages/integrations/package.json),因此需要手动安装底层库:

npm i @vueuse/integrations sortablejs@^1

如果你的项目使用 pnpm,等价命令为:

pnpm add @vueuse/integrations sortablejs@^1

注意版本约束:SortableJS 需要^1主版本系列(@types/sortablejs同步提供类型支持),Vue 版本要求^3.5.0。若只想按需安装useSortable而不引入整个 integrations 包,也可以直接安装后从子模块路径导入——仓库的 exports 映射(packages/integrations/package.json 中的./useSortable与./useSortable/component条目)保证了这种按需导入在打包时是安全的。

四种使用方式与底层原理

方式一:模板引用(Template Ref)——最推荐

<script setup lang="ts"> import { useSortable } from '@vueuse/integrations/useSortable' import { shallowRef, useTemplateRef } from 'vue' const el = useTemplateRef('el') const list = shallowRef([{ id: 1, name: 'a' }, { id: 2, name: 'b' }, { id: 3, name: 'c' }]) useSortable(el, list) </script> <template> <div ref="el"> <div v-for="item in list" :key="item.id"> {{ item.name }} </div> </div> </template>

这是最贴近 Vue 惯例的用法:el是模板引用,list是数据源。从实现上看,useSortable在内部通过tryOnMounted(start)挂载时自动初始化 Sortable 实例(packages/integrations/useSortable/index.ts),并且默认内置了onUpdate处理器:每当拖拽结束,它自动调用moveArrayElement(list, e.oldIndex!, e.newIndex!, e)将数组重排回响应式数据(index.ts)。也就是说,拖拽完成后list会直接反映新的顺序,无需编写任何同步代码。

方式二:指定拖拽手柄(Handle)+ 运行时动态配置选项

<script setup lang="ts"> import { useSortable } from '@vueuse/integrations/useSortable' import { shallowRef, useTemplateRef } from 'vue' const el = useTemplateRef('el') const list = shallowRef([{ id: 1, name: 'a' }, { id: 2, name: 'b' }, { id: 3, name: 'c' }]) const animation = 200 const { option } = useSortable(el, list, { handle: '.handle', }) option('animation', animation) // 运行时设置 // const v = option('animation') // 运行时读取,返回 200 </script> <template> <div ref="el"> <div v-for="item in list" :key="item.id"> <span>{{ item.name }}</span> <span class="handle">*</span> </div> </div> </template>

这里演示了两个核心技巧:

  • handle选项:只有点击.handle元素才能触发拖拽,常用于「列表项内部只有特定区域可拖」的场景;
  • option方法:在实例创建之后动态设置/读取任意 Sortable 选项。

其底层实现(index.ts)是转发给 SortableJS 实例的sortable.option(name, value)/sortable.option(name):传入value时是设置(返回void),省略value时是读取(返回当前值)。由于option通过重载签名约束了name必须是Sortable.Options的合法键(见类型声明UseSortableReturn),运行时的笔误在编译期就会被拦截。仓库自带的可交互演示(packages/integrations/useSortable/demo.vue)正是利用这一机制,用两个按钮在animation: 150与animation: 0之间切换排序动画的开与关。

方式三:CSS 选择器定位根元素

<script setup lang="ts"> import { useSortable } from '@vueuse/integrations/useSortable' import { shallowRef } from 'vue' const list = shallowRef([{ id: 1, name: 'a' }, { id: 2, name: 'b' }, { id: 3, name: 'c' }]) useSortable('#dv', list) </script> <template> <div id="dv"> <div v-for="item in list" :key="item.id"> <span>{{ item.name }}</span> </div> </div> </template>

当容器元素不在当前组件作用域内(例如位于其他组件或动态注入的 DOM 中)时,可以直接传 CSS 选择器字符串。源码中字符串分支通过document?.querySelector(el)解析目标元素(index.ts),且document默认取defaultDocument,同时接受ConfigurableDocument选项以便在非浏览器环境注入自定义 document 对象。

注意事项:字符串分支不会触发watchElement的元素监听(源码只在typeof el !== 'string'时才建立 watch,见 index.ts)。如果目标元素是v-if条件渲染、在挂载后才出现的,start()每次调用都会重新querySelector查一次 DOM——测试用例(packages/integrations/useSortable/index.browser.test.ts 中的string selector regression分组)专门钉住了这一行为:目标元素出现后再调用start()或stop()+start(),都能正确绑定 Sortable。这是挂载时查询失败后的标准自救手段。

方式四:组件式UseSortable

<script setup lang="ts"> import { UseSortable } from '@vueuse/integrations/useSortable/component' import { shallowRef } from 'vue' const list = shallowRef([ { id: 1, name: 'a' }, { id: 2, name: 'b' }, { id: 3, name: 'c' }, ]) </script> <template> <UseSortable v-model="list" as="ol" :options="{ animation: 150 }"> <li v-for="item in list" :key="item.id"> {{ item.name }} </li> </UseSortable> </template>

对于偏好声明式模板的团队,可以引入同名组件UseSortable(子路径@vueuse/integrations/useSortable/component,在 packages/integrations/tsdown.config.ts 构建流程下与函数式实现共享同一份核心逻辑)。该组件的核心机制(packages/integrations/useSortable/component.ts):

  • 通过useVModel(props, 'modelValue')建立v-model双向绑定,拖拽结果直接写回外部list;
  • 通过as属性自定义渲染标签(默认div,示例中为ol),子元素由默认插槽提供;
  • 内部用shallowRef持有目标元素,并将useSortable的返回值放入reactive后暴露给插槽作用域。

插槽作用域可以拿到start、stop、option等控制方法,从而实现「停用/恢复排序」按钮:

<template> <UseSortable v-slot="{ stop, start }" v-model="list"> <button @click="stop()">Stop Sorting</button> <button @click="start()">Start Sorting</button> <div v-for="item in list" :key="item.id"> {{ item.name }} </div> </UseSortable> </template>

返回值:start / stop / option 的完整生命周期

属性说明
start初始化 Sortable 实例(挂载时自动调用)
stop销毁 Sortable 实例
option运行时获取或设置 Sortable 选项
const { start, stop, option } = useSortable(el, list) // 停用排序 stop() // 重新启用排序 start() // 读写选项 option('animation', 200) // 设置 const animation = option('animation') // 读取

结合源码理解其生命周期模型:

  • start会先解析目标元素(字符串走querySelector,其余走unrefElement),再创建new Sortable(target, {...defaultOptions, ...resetOptions})。这里有个值得注意的实现细节:用户传入的options会与内置的默认onUpdate合并,因此自定义onUpdate会覆盖默认的数据同步逻辑——这正是下一节「自定义更新处理器」的入口。
  • stop与组件卸载时的tryOnScopeDispose钩子都会执行cleanup(),即sortable?.destroy()并清空实例引用,避免内存泄漏(index.ts)。
  • 当watchElement为false(默认)时,Sortable 只在挂载时初始化一次,元素引用变化后需要手动start()。

测试用例(packages/integrations/useSortable/index.browser.test.ts)为这三个方法提供了直接验证:stop()后Sortable.get(el)返回null,start()后实例重新出现;option('disabled', true)能正确写入并被读取。这些行为与文档描述一一对应。

watchElement:让 Sortable 跟随元素变化自动重建

当容器元素本身是条件渲染(如v-if切换)时,默认策略会让 Sortable 实例停留在旧的(可能已卸载的)元素上。此时开启watchElement即可自动跟随:

import { useSortable } from '@vueuse/integrations/useSortable' useSortable(el, list, { watchElement: true, // 元素变化时自动重新初始化 })

源码实现(index.ts)在watchElement: true且目标为元素引用(非字符串)时,会用watch(() => unrefElement(el), ..., { immediate: true, flush: 'post' })监听元素引用:元素变化时先cleanup()销毁旧实例,再对新元素initSortable重新初始化,immediate: true保证组件一挂载就完成首轮初始化。

watchElement的行为差异在测试中有非常直观的对照(index.browser.test.ts 的watchElement分组):

  • 开启时,v-if从true切到false再切回true,新元素上的 Sortable 会被自动重新初始化;
  • 关闭时(默认),新元素上没有 Sortable,旧实例仍绑定在已移除的元素上,必须手动stop()+start()才能在新元素上重建实例。

因此,凡涉及条件渲染、动态挂载/卸载容器的场景,优先开启watchElement: true可以省去手动管理实例的繁琐工作。

自定义更新处理器:接管数据移动逻辑

默认情况下useSortable内置了onUpdate,拖拽完成后自动重排数组。若你需要在上报数据前做额外处理(例如记录操作日志、触发异步请求、做位置校验),可以传入自定义onUpdate——此时默认处理器被覆盖,数组移动需要你自行调用暴露的moveArrayElement完成:

import { moveArrayElement, useSortable } from '@vueuse/integrations/useSortable' useSortable(el, list, { onUpdate: (e) => { // 自定义业务逻辑 moveArrayElement(list, e.oldIndex, e.newIndex, e) // moveArrayElement 在微任务中执行,因此这里需要 nextTick // 等待其完成后再继续 nextTick(() => { /* do something */ }) } })

这里的关键细节是时序:moveArrayElement对ref类型的数组采用「先浅拷贝、再在nextTick中完成splice写入」的策略(index.ts),以避免移动元素时反复触发副作用。因此文档明确提示:如果你需要在重排完成后立即读取最新数组,请放在nextTick回调中执行。

辅助函数:可单独使用的工具

useSortable模块还导出了三个独立辅助函数(均有完整类型声明与源码实现):

函数说明
moveArrayElement(list, from, to, event?)将数组中的元素从from索引移动到to索引
insertNodeAt(parent, element, index)在parent的指定索引位置插入一个 DOM 节点
removeNode(node)从父节点中移除一个 DOM 节点

它们的设计意图与moveArrayElement的调用方式互相呼应:

  • moveArrayElement在传入event参数时,会先通过removeNode(e.item)把被拖拽的真实 DOM 节点从原位置移除,再通过insertNodeAt(e.from, e.item, from)按原索引插回父节点,从而保持 DOM 结构稳定,之后再异步完成数组索引的移动(index.ts);
  • insertNodeAt基于parentElement.children[index]定位参照节点,用insertBefore完成插入,索引越界时即退化为追加到末尾;
  • removeNode在节点存在父节点时执行removeChild,带空指针保护。

三者组合使用,即可在没有 Sortable 的情况下手工编排 DOM 与数组的同步移动。

类型声明速览

useSortable对外暴露的完整类型(与源码中的UseSortableReturn、UseSortableOptions一一对应,见 index.ts):

  • UseSortableReturn:包含start(): void、stop(): void,以及重载的option<K extends keyof Sortable.Options>(name, value)(设置)与option<K>(name)(读取)双形态;
  • UseSortableOptions:继承Sortable.Options与ConfigurableDocument,额外提供watchElement?: boolean,默认false——即默认只在挂载时初始化一次;
  • 函数重载:useSortable同时接受selector: string与el: MaybeRefOrGetter<MaybeElement>两种首参形态,list则统一为MaybeRef<T[]>(普通数组或响应式 ref 均可)。

总结

useSortable把 SortableJS 的实例管理与 Vue 的响应式数据桥接压缩成了一个可组合函数:默认的onUpdate自动完成数组重排,option()提供运行时的选项读写,watchElement处理条件渲染下的实例重建,而moveArrayElement/insertNodeAt/removeNode三个辅助函数则让精细控制成为可能。结合 index.ts 的源码与 index.browser.test.ts 的测试覆盖,你可以放心地在生产项目中使用它实现单列表拖拽排序;至于跨列表拖拽,则需等待后续版本或自行扩展。

  • 前端

【免费下载链接】vueuse

Collection of essential Vue Composition Utilities for Vue 3

项目地址:https://gitcode.com/gh_mirrors/vu/vueuse
点击查看免费下载

相关推荐

上一篇:Diablo Edit2终极指南:打造完美暗黑破坏神2角色的完整解决方案
下一篇:终极网盘直链下载助手完整指南:5分钟告别限速,一键获取真实下载链接

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 15:57:37

GhostTrack 免费安装教程:5 分钟跑通 IP 查询

GhostTrack 免费安装教程&#xff1a;5 分钟跑通 IP 查询 【免费下载链接】GhostTrack Useful tool to track location or mobile number 项目地址: https://gitcode.com/GitHub_Trending/gh/GhostTrack GhostTrack 是一款免费的 Python 小工具&#xff0c;主打 OSINT 信…

作者头像 李华
网站建设 2026/10/6 15:48:49

为 .NET 客户端接入 LLM:mcp-for-beginners 03-llm-client 实战指南

教程文档人工智能 【免费下载链接】mcp-for-beginners This open-source curriculum introduces the fundamentals of Model Context Protocol (MCP) through real-world, cross-language examples in .NET, Java, TypeScript, JavaScript, Rust and Python. Designed for deve…

作者头像 李华
网站建设 2026/10/6 15:40:24

视频目标跟踪标注避坑指南:EasyDL关键帧与消失帧实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/6 15:39:40

微小型双足机器人强化学习实战:从仿真训练到真机部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/6 15:35:56

ESP32-C5硬件设计指南:从原理图到PCB Layout的完整避坑实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华