皮皮网
皮皮网

【无源码检测】【人才招聘网站源码】【舆情分析系统源码】sortable源码分析

时间:2024-12-27 19:48:30 来源:智联代理源码

1.Vue中使用Sortable
2.SortableJS原理分析(源码)
3.使用 SortableJS 组件的源码 Blazor 可排序列表

sortable源码分析

Vue中使用Sortable

        之前开发一个后台管理系统,里面用到了 Vue 和 Element-UI 这个组件库,遇到一个挺有意思的问题,和大家分享一下。

        场景是这样,在一个列表展示页上,我使用了 Element-UI 的表格组件,新的需求是在原表格的基础上支持拖拽排序。但是原有的组件本身不支持拖拽排序,而且由于是直接引入的 Element-UI ,不方便修改它的源码,所以比较可行的方法只能是直接操作DOM。

        具体的做法是在 mounted 生命周期函数里,对 this.$el 进行真实DOM的操作,监听 drag 的一系列事件,在事件回调里移动DOM,并更新data。

        HTML5 Drag 事件还是挺多的,和 Touch 事件差不多,自己手工实现也可以,不过这里就偷了个懒,直接用了一个开源的 Sortable 库,直接传入 this.$el ,监听封装后的回调,并且根据Vue的开发模式,在移动DOM的回调里更新实际的Data数据,保持数据和DOM的一致性。

        如果你以为到这就结束了,那就大错特错,偷过的懒迟早要还。。。本以为这个方案是很美好的,没想到刚想调试一下,就出现了诡异的现象:A和B拖拽交换位置之后,B和A又神奇得换回去了!这是怎么回事?似乎我们的操作没有什么问题,在真实DOM移动了之后,我们也移动了相应的 data ,数据数组的顺序和渲染出DOM的顺序应该是一致的。

        问题出在哪里?我们回忆一下Vue的实现原理,在Vue2.0之前是通过 defineProperty 依赖注入和跟踪的方式实现双向绑定。针对v-for数组指令,如果指定了唯一的Key,则会通过高效的Diff算法计算出数组内元素的差异,进行最少的移动或删除操作。而Vue2.0之后在引入了 Virtual Dom 之后, Children 元素的 Dom Diff 算法和前者其实是相似的,唯一的区别就是,2.0之前Diff直接针对 v-for 指令的数组对象,2.0之后则针对 Virtual Dom 。DOM Diff算法在这里不再赘述,这里解释的比较清楚 virtual-dom diff算法

        假设我们的列表元素数组是

        渲染出来后的DOM节点是

        那么Virtual Dom对应的结构就是

        假设拖拽排序之后,真实的DOM变为

        此时我们只操作了真实DOM,改编了它的位置,而Virtual Dom的结构并没有改变,依然是

        此时我们把列表元素也按照真实DOM排序后变成

        这时候根据Diff算法,计算出的Patch为,VNode前两项是同类型的节点,所以直接更新,即把$A节点更新成$B,把$B节点更新成$A,真实DOM又变回了

        所以就出现了拖拽之后又被Patch算法更新了一次的问题,操作路径可以简单理解为

        根本原因是 Virtual DOM 和 真实DOM 之间出现了不一致。

        所以在Vue2.0以前,因为没有引入 Virtual DOM ,这个问题是不存在的。

        在使用Vue框架的时候要尽量避免直接操作DOM

        3.暴力解决!不走patch更新,通过v-if设置,直接重新渲染一遍。当然不建议这么做,只是提供这种思路~

        所以,我们平时在使用框架的时候,也要去了解框架的实现原理的,否则遇到一些棘手的情况就会无从下手~

SortableJS原理分析(源码)

       前言

       SortableJS是基于H5拖拽API实现的一个轻量级JS拖拽排序库,它适用于以下一些场景:

       容器项目拖动排序:容器列表内的分析子项目,通过拖动进行位置调换,源码且具有动画效果;

       容器间的分析项目移动:将一个容器列表中的子项目,拖动到另一个容器列表中(移动/克隆)。源码

       不论是分析无源码检测容器内元素顺序排序,或是源码两个容器内的元素进行移动,本质上是分析在通过操作DOM来实现。

       下面我们先熟悉一下SortableJS基本使用。源码

示例

       1、分析HTML结构:

<divclass="row"><divid="leftContainer"class="list-groupcol-6"><divclass="list-group-item">Item1</div><divclass="list-group-item">Item2</div><divclass="list-group-item">Item3</div><divclass="list-group-item">Item4</div><divclass="list-group-item">Item5</div><divclass="list-group-item">Item6</div></div><divid="rightContainer"class="list-groupcol-6"><divclass="list-group-itemtinted">Item1</div><divclass="list-group-itemtinted">Item2</div><divclass="list-group-itemtinted">Item3</div><divclass="list-group-itemtinted">Item4</div><divclass="list-group-itemtinted">Item5</div><divclass="list-group-itemtinted">Item6</div></div></div>

       2、源码为容器实例化:

newSortable(leftContainer,分析{ group:{ name:'group',pull:'clone',put:true},});newSortable(rightContainer,{ group:'group',});

       现在,就可以在容器内进行排序拖动,源码或者拖动左侧容器元素,分析添加到右侧容器中。源码

思路分析

       在看源码之前,还是需要对H5拖拽用法有一定了解,如果不熟悉,直接去看源码很容易就放弃。人才招聘网站源码

       若你对H5拖拽API比较熟悉,就可以根据SortableJS的视图呈现效果,想出个大概思路。

       拖拽,首先要搞清楚两个词汇对象:

       拖动元素:作为拖拽元素被拖起(下文叫dragEl);

       目标元素:作为拖拽元素即将被放置时的参照物(下文叫target);

       在SortableJS中,拖拽离不开以下几个事件:

       dragstart:作为拖拽元素,按下鼠标开始拖动元素时触发(拖拽周期只触发一次);

       dragend:作为拖拽元素,当鼠标松开拖放元素时触发(拖拽周期只触发一次);

       dragover:作为拖拽元素,当拖动元素进行移动,会持续触发,需要在这里取消默认事件,否则元素无法被拖动(松开时元素的预览幽灵图又回去了);

       drop:作为目标元素,当鼠标松开拖放元素时触发(拖拽周期只触发一次);

       下面我们一起去分析SortableJS具体实现。

源码实例构造函数

       从上面的示例使用上得知,SortableJS是一个构造函数,接收容器元素和配置项:

constexpando='Sortable'+(newDate).getTime();functionSortable(el,options){ this.el=el;//rootelementthis.options=options=Object.assign({ },options);el[expando]=this;constdefaults={ group:null,sort:true,//默认容器可以排序animation:0,removeCloneOnHide:true,//将一个容器元素拖动至另一个容器后,默认setData:function(dataTransfer,dragEl){ dataTransfer.setData('Text',dragEl.textContent);}};//参数合并for(varnameindefaults){ !(nameinoptions)&&(options[name]=defaults[name]);}//规范group_prepareGroup(options);//绑定原型方法为私有方法for(varfninthis){ if(fn.charAt(0)==='_'&&typeofthis[fn]==='function'){ this[fn]=this[fn].bind(this);}}//绑定指针触摸事件,类似mousedownon(el,舆情分析系统源码'pointerdown',this._prepareDragStart);on(el,'dragover',this);on(el,'dragenter',this);}

       初始化示例做了以下几件事件:

       将传入的参数与提供的默认参数进行合并;

       规范传入的group格式;

       将原型上的方法绑定在实例对象上,便于使用;

       绑定pointerdown、dragover、dragenter事件,其中pointerdown可以看作是dragstart事件,做了一些拖拽前的准备工作。

       group用于两个容器元素的相互拖拽场景,规范group核心代码如下:

function_prepareGroup(options){ functiontoFn(value,pull){ returnfunction(to,from){ letsameGroup=to.options.group.name&&from.options.group.name&&to.options.group.name===from.options.group.name;if(value==null&&(pull||sameGroup)){ returntrue;}elseif(value==null||value===false){ returnfalse;}elseif(pull&&value==='clone'){ returnvalue;}else{ returnvalue===true;}};}letgroup={ };letoriginalGroup=options.group;if(!originalGroup||typeoforiginalGroup!='object'){ originalGroup={ name:originalGroup};}group.name=originalGroup.name;group.checkPull=toFn(originalGroup.pull,true);group.checkPut=toFn(originalGroup.put);options.group=group;}_prepareDragStart拖动前的准备工作

       当鼠标按下触发pointerdown事件时,会保存拖动元素的信息,提供后续使用,并且注册dragstart事件:

letoldIndex,newIndex;letdragEl=null;//拖拽元素letrootEl=null;//容器元素letparentEl=null;//拖拽元素的父节点letnextEl=null;//拖拽元素下一个元素letactiveGroup=null;//options.groupSortable.prototype={ _prepareDragStart(evt){ lettarget=evt.target,el=this.el,options=this.options;oldIndex=index(target);rootEl=el;dragEl=target;parentEl=dragEl.parentNode;nextEl=dragEl.nextSibling;activeGroup=options.group;dragEl.draggable=true;//设置元素拖拽属性on(dragEl,'dragend',this);on(rootEl,'dragstart',this._onDragStart);on(document,'mouseup',this._onDrop);},}

       on就是addEventListener,index方法用于获取元素在父容器内的索引:

functionon(el,event,fn){ el.addEventListener(event,fn);}functionoff(el,event,fn){ el.removeEventListener(event,fn);}functionindex(el){ if(!el||!el.parentNode)return-1;letindex=0;//返回元素节点之前的兄弟元素节点(不包括文本节点、注释节点)while(el=el.previousElementSibling){ if(el!==Sortable.clone)index++;}returnindex;}

       _onDragStart用于处理dragstart事件逻辑,_onDrop用于处理拖拽结束逻辑,比如这里执行了dragEl.draggable=true;,那么在mouseup鼠标松开后需将draggable=false。

       这里有趣的一点是dragend事件,它的处理函数绑定的是this即Sortable实例本身,我们都知道实例对象是箱体捉妖指标公式源码一个对象,怎么能作为函数使用呢?

       其实addEventListener第二参数可以是函数,也可以是对象,当为对象时,需要提有一个handleEvent方法来处理事件:

Sortable.prototype={ handleEvent:function(evt){ switch(evt.type){ case'dragend':this._onDrop(evt);break;case'dragover':evt.stopPropagation();evt.preventDefault();break;case'dragenter':if(dragEl){ this._onDragOver(evt);}break;}},}

       到这里,整个拖拽流程功能函数都暴露在了眼前:

       _onDragStart处理dragstart拖拽开始工作;

       _onDragOver处理拖拽移动到别的元素时工作;

       _onDrop处理鼠标拖动结束的收尾工作。

dragstart

       这里做了两件事情:

       clone一个dragEl元素副本,用于两个容器项目移动时使用;

       触发外部传入的clone和dragstart事件;

letcloneEl=null,cloneHidden=null;//clone元素_onDragStart(evt){ letdataTransfer=evt.dataTransfer;letoptions=this.options;cloneEl=clone(dragEl);cloneEl.removeAttribute("id");cloneEl.draggable=false;//设置拖拽数据if(dataTransfer){ dataTransfer.effectAllowed='move';options.setData&&options.setData.call(this,dataTransfer,dragEl);}Sortable.active=this;Sortable.clone=cloneEl;_dispatchEvent({ sortable:this,name:'clone'});_dispatchEvent({ sortable:this,name:'start',originalEvent:evt});},functionclone(el){ returnel.cloneNode(true);}

       _dispatchEvent会通过newwindow.CustomEvent构造一个事件对象,将拖拽元素的信息添加到自定义事件对象上,传递给外部的注册事件函数,大体代码如下:

functiondispatchEvent(...params){ //sortable没有传,就根据rootEl获取sortable。sortable=(sortable||(rootEl&&rootEl[expando]));if(!sortable)return;letevt,options=sortable.options,onName='on'+name.charAt(0).toUpperCase()+name.substr(1);//自定义事件,拿到事件对象,满足外部用户传入的事件正常使用if(window.CustomEvent){ evt=newCustomEvent(name,{ bubbles:true,cancelable:true});}else{ evt=document.createEvent('Event');evt.initEvent(name,true,true);}evt.to=toEl||rootEl;evt.from=fromEl||rootEl;evt.item=targetEl||rootEl;evt.clone=cloneEl;evt.oldIndex=oldIndex;evt.newIndex=newIndex;//执行外部传入的事件if(options[onName]){ options[onName].call(sortable,evt);}}

       可见,拖拽的核心逻辑不在dragstart中,下面我们去看dragenter的处理函数_onDragOver。

dragenter

       SortableJS的核心逻辑在_onDragOver中,拿容器内项目排序为例:当拖动dragEl元素,jdk1.8源码包移动到另一个元素上时,会发生两者的位置交换,可见,Sort的逻辑在这里。

       首先,在实例化对象时绑定了dragover和dragenter事件,并且通过handleEvent将事件逻辑交由_onDragOver来处理:

on(el,'dragover',this);on(el,'dragenter',this);handleEvent:function(evt){ switch(evt.type){ case'dragover':evt.stopPropagation();evt.preventDefault();break;case'dragenter':if(dragEl){ this._onDragOver(evt);}break;}},

       在_onDragOver中,需要注意一点是:假如有两个容器,那就有两个newSortable实例对象,isOwner将为false,这是就需要判断拖动容器的activeGroup.pull(是否允许被移动)和group.put(是否允许添加拖动过来的元素)。

newSortable(leftContainer,{ group:{ name:'group',pull:'clone',put:true},});newSortable(rightContainer,{ group:'group',});0

       上面的核心在于下面这一行代码:

newSortable(leftContainer,{ group:{ name:'group',pull:'clone',put:true},});newSortable(rightContainer,{ group:'group',});1

       如果拖拽元素的位置小于目标元素的位置,说明是从上往下拖动,那么将dragEl移动到target.nextSibling之前;

       如果拖拽元素的位置大于目标元素的位置,说明是从下往上拖动,那么只需将dragEl移动到target之前即可;

       整个移动过程均采用DOM操作insertBefore来实现。

       另外如果是两个容器的场景(isOwner=false),并且拖动元素的容器activeGroup.pull=clone,需要将dragstart创建的clone元素渲染到容器中:

newSortable(leftContainer,{ group:{ name:'group',pull:'clone',put:true},});newSortable(rightContainer,{ group:'group',});2drop

       drop主要做一些收尾工作,如将dragEl.draggable=false,移除绑定的mouseup、dragstart、dragend事件,触发用户传入的sort、end事件等。

       不过注意,虽然起名叫drop,触发的事件确是dragend。

newSortable(leftContainer,{ group:{ name:'group',pull:'clone',put:true},});newSortable(rightContainer,{ group:'group',});3动画

       如果想在拖动排序中有一定的animation动画效果,可以配置动画属性,属性值是动画持续时长:

newSortable(leftContainer,{ group:{ name:'group',pull:'clone',put:true},});newSortable(rightContainer,{ group:'group',});4

       动画的时机也是在dragenter中,大致的思路如下:

       1、记录:记录容器子项位置信息

       在操作DOM移动dragEl之前,记录容器内所有子项的位置;

       进行DOM操作进行位置交换,DOM操作本身没有动画;

       这时再去记录一次移动后的容器内所有子项的位置;

       2、执行:有了上面几步的操作,接下来就可以根据移动前后的位置进行动画操作

       通过translate先让元素立刻回到移动前的位置;

       此时给元素自身设置过度效果transform;

       这时候就可以通过translate让元素回到移动之后的位置。

       大致实现如下:

newSortable(leftContainer,{ group:{ name:'group',pull:'clone',put:true},});newSortable(rightContainer,{ group:'group',});5最后

       本文以探索SortableJS拖拽思路为主线,去了解业界开源拖拽库的设计与思路。感谢阅读。

原文:/post/

使用 SortableJS 组件的 Blazor 可排序列表

       作者:Burke Holland 排版:Alan Wang

       在 Web 应用程序中,一个常见功能部分是可排序列表。Blazor 开发者们可能怀念 SortableJS 这个强大的 JavaScript 库。为了填充这个空白,Burke Holland 开发了一个名为 Blazor Sortable 的组件,将其开源在 GitHub 上,供开发者使用。

       Blazor Sortable 是一个社区组件,不是 Microsoft 的官方组件,但它提供了一种将 SortableJS 的功能集成到 Blazor 应用中的方法。Fluent UI 团队正在努力在未来版本的 Blazor 中集成可排序组件。

       访问演示页面:[提供演示链接]

       Burke 和 Jon Galloway,通过“Burke Learns Blazor”直播在 Twitch 和 .NET YouTube 上,共同致力于重建 theurlist.com 应用程序。如果您愿意加入他们的行列,提供帮助,将会非常欢迎。

       在 Blazor 中构建可排序列表组件的需求促使 Burke 开发了 Blazor Sortable。SortableJS 提供了丰富的功能,包括排序、列表之间的排序、项目克隆、过滤、自定义动画、腰部支持等。

       Blazor Sortable 包含了源代码和演示,用户只需要下载 Shared/SortableList.razor、Shared/SortableList.razor.css 和 Shared/SortableList.razor.js 文件。SortableList 组件是一个通用组件,需要提供项目列表和定义如何呈现列表中每个项目的模板。

       为了使列表变得可排序,只需处理 OnUpdate 事件并自定义排序逻辑。每当列表排序时,OnUpdate 事件会传递包含已移动项目旧索引和新索引的元组,通过 SortList 方法解构元组并移动列表中的项目。

       Blazor Sortable 支持多种功能,如列表之间的排序、克隆项目、过滤项目等。Burke 提供了一个示例,展示了如何在两个列表之间进行排序。

       在设置 SortableList 的样式时,您可以覆盖默认样式以适应特定需求。默认样式会隐藏“ghost”元素,产生拖动间隙。您可以修改 SortableItemTemplate 子项内的样式,但必须使用“::deep”修饰符确保样式生效。

       HTML5 的原生拖放支持在某些方面有局限性,无法提供理想的排序体验。因此,Blazor Sortable 选择使用 SortableJS 的 JavaScript 解决方案。如果您希望重新启用 HTML5 支持,只需在 SortableList.razor.razor.js 文件中删除 forceFallback: true 属性。

       Blazor Sortable 是一个开源社区项目,欢迎开发者探索其功能并提供反馈。Burke 鼓励大家使用 Blazor Sortable 来创建具有高级排序功能的 Blazor 应用程序。您可以通过 GitHub 页面查看 Blazor Sortable 并分享您的想法。

更多内容请点击【热点】专栏