在Web开发领域,JavaScript(简称JS)作为一门重要的前端脚本语言,被广泛应用于网页设计和互动性增强。对于开发者而言,深入理解JS源码,掌握其核心原理,是提升编程技能的关键。本文将围绕多选这一功能,探讨JavaScript源码中的奥秘与技巧。
一、多选功能概述
多选功能是指用户在列表或表格中选择多个选项的功能。在JavaScript源码中,实现多选功能通常涉及以下步骤:
1. 创建多选控件,如复选框(checkbox);
2. 使用JavaScript获取多选控件的状态;
3. 根据用户的选择,进行相应的处理,如统计选中项、执行操作等。
二、多选功能实现原理
1. 多选控件
在HTML中,可以使用复选框实现多选功能。复选框是一种单选/多选控件,通过设置其属性,可以控制其行为。以下是一个简单的复选框示例:
```html
Day.js 源码解析答案:
概念: Day.js 是一款轻量级的时间库,主打无需引入过多依赖,以减少打包体积的特性。 理解基础概念至关重要,包括时间标准等,这些为后续分析提供了背景知识。 GMT 作为本初子午线上的平太阳时,而 UTC 是基于原子时标准,与 GMT 关系密切。 ISO 8601 是国际标准化组织推荐的日期和时间表示方法,JavaScript 中的 Date.prototype.toISOString 方法返回遵循 ISO 8601 标准的字符串。
locale: Day.js 通过 locale 实现了多语言支持,用户可根据需求引入相应的语言包。 不同语言对时间的描述各具特色,locale 机制使得 Day.js 能够适应不同语言环境下的时间表示需求。 语言代码与 locale 关联,用户可按需加载特定语言包,以满足本地化需求。
constant: src/constant.js 文件负责存储常量。 这些常量包括时间单位与格式化的正则表达式等,是 Day.js 进行时间操作和格式化的基础。
utils: src/utils.js 文件封装了一系列实用工具函数。 这些工具函数用于简化时间操作,如日期解析、时间计算等,提高了 Day.js 的灵活性和易用性。 工具函数的封装使得 Day.js 的代码更加模块化和可维护。
tags: 核心逻辑:Day.js 的核心逻辑主要体现在其轻量级、按需加载和模块化设计上。 设计思路:Day.js 的设计思路是提供简洁、高效的时间操作接口,同时保持较小的打包体积。 源码结构:Day.js 的源码结构清晰明了,主要依赖集中在入口文件中,便于开发者理解和扩展。
Slate.js的渲染机制主要包括以下几个方面:
文档结构和节点:
Slate文档的结构包含元素和文本两类节点,这些节点类似于DOM树,可以嵌套结构。用户在元素或文本上添加扩展属性,以提供渲染节点所需的数据。组件树与渲染:
Slate组件树类似于DOM树,对应于Slate值的数据结构。节点数据被渲染为HTML,允许用户自定义渲染过程,通过renderElement方法实现。文本内容的渲染通过renderLeaf方法来控制文本内容的样式。实时渲染与事件监听:
Slate值的更新逻辑利用React技术,将文档数据实时渲染为DOM结构。当contenteditable为true的元素被修改时,会触发beforeInput事件,通过监听这一事件,实现文档内容的实时同步。输入法处理:
正常键盘输入仅触发beforeInput事件,而使用输入法时,还会触发Composition事件。在输入法输入期间,如果实时修改文档内容,会导致与输入法冲突。因此,在CompositionUpdate期间,Slate Value不会做任何更新,直至CompositionEnd时再进行更新。解决方法之一是在CompositionStart时更新文档值以避免冲突,或者通过fork源码来确保Slate与输入法协同工作。选择机制:
Slate Selection数据结构与DOM Selection类似,由锚点和焦点两个点组成。Selection的更新机制依赖于React完成渲染,在每次Selection值发生变化时,会在useEffect中更新DOMSelection。同时,监听window.document上的selectionchange事件以更新Slate Selection值。这些机制共同构成了Slate.js的渲染体系,使得Slate能够高效地处理富文本编辑任务,并提供良好的用户体验。
前言
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。
dragenterSortableJS的核心逻辑在_onDragOver中,拿容器内项目排序为例:当拖动dragEl元素,移动到另一个元素上时,会发生两者的位置交换,可见,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',});2dropdrop主要做一些收尾工作,如将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拖拽思路为主线,去了解业界开源拖拽库的设计与思路。感谢阅读。
原文:文章已关闭评论!
2025-05-09 23:05:13
2025-05-09 22:45:22
2025-05-09 22:33:25
2025-05-09 22:22:51
2025-05-09 22:01:11
2025-05-09 21:15:02
2025-05-09 21:02:22
2025-05-09 20:43:13