- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
导读
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
相关推荐
VueUse useSortable 实战指南:在 Vue 3 中优雅集成 SortableJS 拖拽排序
VueUse useSortable 实战指南:在 Vue 3 中优雅集成 SortableJS 拖拽排序 useSortable 是 VueUse 对 Sor
前端在 airi 的 Vue 3 应用中用 VueUse useSortable 实现拖拽排序:完整实战与源码级解读
在 airi 的 Vue 3 应用中用 VueUse useSortable 实现拖拽排序:完整实战与源码级解读 useSortable 是 VueUse 对
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染FGO-py:告别重复操作,让《命运/冠位指定》智能自动化的终极方案
FGO py:告别重复操作,让《命运/冠位指定》智能自动化的终极方案 还在为《命运/冠位指定》(FGO)中无尽的刷本、抽卡、日常任务而烦恼吗?每天花费数小时在重
GUI 自动化桌面应用计算机视觉RPA任务调度
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考