从零实现JS原生拖拽插件:原理、封装与优化指南

作者:缓存笔记

时间:2026-10-29

浏览:639

本文从零开始解析JavaScript拖拽插件的开发过程。详细阐述基于mousedown、mousemove和mouseup事件的拖拽原理,演示如何将基础逻辑封装为可复用的全局Drag方法。内容涵盖多元素管理、有效拖拽区域判定以及利用setTimeout提升onmousemove执行效率的性能优化技巧,并提供jQuery插件化思路。

文章目录

本文深入解析JavaScript拖拽插件的开发流程,主要围绕六个核心环节展开详细论述。

JS拖拽插件原理与基础实现

核心内容涵盖:一、JS拖拽插件的运行原理;二、基于原理的基础效果实现;三、代码的抽象与优化处理;四、有效拖拽元素的扩展功能;五、性能优化措施及总结;六、jQuery插件化改造。JS拖拽是网页交互中的常见效果,本文将引导读者从零开始构建一个简单的JS插件。

一、JS拖拽插件的运行原理 典型的拖拽交互包含以下主要步骤:

1、用户鼠标点击需要拖拽的目标元素。

2、保持鼠标按键按下状态,并移动鼠标指针。

3、将元素移动至指定位置后,松开鼠标按键。

该过程涉及三个关键的DOM事件:onmousedown、onmousemove和onmouseup。因此,拖拽的基本实现逻辑如下:

1、点击目标元素触发onmousedown事件。

(1)将当前元素的可拖拽状态标记为true,标识该元素允许被拖拽。

(2)记录此时鼠标在页面上的坐标x和y。

(3)记录当前目标元素在页面上的坐标x和y。

2、移动鼠标触发onmousemove事件。

(1)判定元素是否具备拖拽属性,若满足条件则执行步骤2,否则直接终止流程

(2)若元素可拖拽,则更新元素的坐标位置。

元素的新x坐标 = 鼠标横向移动距离 + 元素原始x坐标 = 鼠标当前x坐标 - 鼠标初始x坐标 + 元素原始x坐标。

元素的新y坐标 = 鼠标纵向移动距离 + 元素原始y坐标 = 鼠标当前y坐标 - 鼠标初始y坐标 + 元素原始y坐标。

3、松开鼠标触发onmouseup事件。

(1)将可拖拽状态重置为false。

回到顶部 二、基于原理的基础效果实现 在编写基础代码前,需注意以下几点:

1、目标元素必须设置position属性为relative或absolute,否则无法通过改变left/top实现位移。

2、使用event.clientX和event.clientY属性获取鼠标相对于视口的坐标。

3、onmousemove事件应绑定在document对象上,而非拖拽元素本身,这样可以避免鼠标移动过快时脱离元素导致拖拽中断或延迟的问题。

具体实现代码如下:


var dragObj = document.getElementById("test");
   dragObj.style.left = "px";
   dragObj.style.top = "px";
 
   var mouseX, mouseY, objX, objY;
   var dragging = false;
 
   dragObj.onmousedown = function (event) {
    event = event || window.event;
 
    dragging = true;
    dragObj.style.position = "relative";
 
 
    mouseX = event.clientX;
    mouseY = event.clientY;
    objX = parseInt(dragObj.style.left);
    objY = parseInt(dragObj.style.top);
   }
 
   document.onmousemove = function (event) {
    event = event || window.event;
    if (dragging) {
 
     dragObj.style.left = parseInt(event.clientX - mouseX + objX) + "px";
     dragObj.style.top = parseInt(event.clientY - mouseY + objY) + "px";
    }
 
   }
 
   document.onmouseup = function () {
    dragging = false;
   }


三、代码抽象与优化

代码抽象与全局封装

为了将上述代码封装为通用插件,需进行抽象处理,基本结构如下:

 

代码实现如下:

; (function (window, undefined) {
 
function Drag(ele) {}
 
window.Drag = Drag;
})(window, undefined);

通过自执行匿名函数包裹代码,内部定义Drag方法并将其暴露至全局作用域。使用时直接调用Drag,并传入需要拖拽的元素节点即可。

首先对常用方法进行简单封装:


; (function (window, undefined) {
    var dom = {
     //绑定事件
     on: function (node, eventName, handler) {
      if (node.addEventListener) {
       node.addEventListener(eventName, handler);
      }
      else {
       node.attachEvent("on" + eventName, handler);
      }
     },
     //获取元素的样式
     getStyle: function (node, styleName) {
      var realStyle = null;
      if (window.getComputedStyle) {
       realStyle = window.getComputedStyle(node, null)[styleName];
      }
      else if (node.currentStyle) {
       realStyle = node.currentStyle[styleName];
      }
      return realStyle;
     },
     //获取设置元素的样式
     setCss: function (node, css) {
      for (var key in css) {
       node.style[key] = css[key];
      }
     }
    };
    window.Drag = Drag;
   })(window, undefined);

在拖拽操作中,主要涉及两个对象:被拖拽的元素对象和鼠标对象。我们定义如下两个对象及其对应操作方法:

首先是拖拽元素对象,它包含一个DOM元素节点以及拖拽前的x和y坐标:


 function DragElement(node) {
     this.node = node;//被拖拽的元素节点
     this.x = ;//拖拽之前的x坐标
     this.y = ;//拖拽之前的y坐标
    }
    DragElement.prototype = {
     constructor: DragElement,
     init: function () {     
      this.setEleCss({
       "left": dom.getStyle(node, "left"),
       "top": dom.getStyle(node, "top")
      })
      .setXY(node.style.left, node.style.top);
     },
     //设置当前的坐标
     setXY: function (x, y) {
      this.x = parseInt(x) || ;
      this.y = parseInt(y) || ;
      return this;
     },
     //设置元素节点的样式
     setEleCss: function (css) {
      dom.setCss(this.node, css);
      return this;
     }
    }

另一个对象是鼠标对象,主要包含x坐标和y坐标:


function Mouse() {
     this.x = ;
     this.y = ;
    }
    Mouse.prototype.setXY = function (x, y) {
     this.x = parseInt(x);
     this.y = parseInt(y);
    }  

以上是在拖拽操作中定义的两个核心对象。

若页面中存在多个可拖拽元素,需注意以下事项:

1、每个元素对应一个独立的拖拽对象实例。

2、同一时刻页面中只能有一个元素处于拖拽状态。

为此,我们定义一个全局配置对象来保存相关状态:

 

代码实现如下:

var draggableConfig = {
zIndex: ,
draggingObj: null,
mouse: new Mouse()
             };

该对象包含三个属性:

(1)zIndex:用于赋值给拖拽对象的zIndex属性。当多个拖拽对象重叠时,通过设置zIndex确保当前拖拽对象显示在最顶层。

(2)draggingObj:用于保存当前正在拖拽的对象。此处移除了之前用于判断是否可拖拽的独立变量,改为通过draggingObj是否存在来判断当前是否允许拖拽,并获取相应的拖拽对象。

(3)mouse:唯一的鼠标对象,用于保存当前鼠标的坐标等信息。

最后绑定onmousedown、onmouseover和onmouseout事件,整合后的代码如下:


 ; (function (window, undefined) {
    var dom = {
     //绑定事件
     on: function (node, eventName, handler) {
      if (node.addEventListener) {
       node.addEventListener(eventName, handler);
      }
      else {
       node.attachEvent("on" + eventName, handler);
      }
     },
     //获取元素的样式
     getStyle: function (node, styleName) {
      var realStyle = null;
      if (window.getComputedStyle) {
       realStyle = window.getComputedStyle(node, null)[styleName];
      }
      else if (node.currentStyle) {
       realStyle = node.currentStyle[styleName];
      }
      return realStyle;
     },
     //获取设置元素的样式
     setCss: function (node, css) {
      for (var key in css) {
       node.style[key] = css[key];
      }
     }
    };
    //#region 拖拽元素类
    function DragElement(node) {
     this.node = node;
     this.x = ;
     this.y = ;
    }
    DragElement.prototype = {
     constructor: DragElement,
     init: function () {     
      this.setEleCss({
       "left": dom.getStyle(node, "left"),
       "top": dom.getStyle(node, "top")
      })
      .setXY(node.style.left, node.style.top);
     },
     setXY: function (x, y) {
      this.x = parseInt(x) || ;
      this.y = parseInt(y) || ;
      return this;
     },
     setEleCss: function (css) {
      dom.setCss(this.node, css);
      return this;
     }
    }
    //#endregion
    //#region 鼠标元素
    function Mouse() {
     this.x = ;
     this.y = ;
    }
    Mouse.prototype.setXY = function (x, y) {
     this.x = parseInt(x);
     this.y = parseInt(y);
    }
    //#endregion
    //拖拽配置
    var draggableConfig = {
     zIndex: ,
     draggingObj: null,
     mouse: new Mouse()
    };
    function Drag(ele) {
     this.ele = ele;
     function mouseDown(event) {
      var ele = event.target || event.srcElement;
      draggableConfig.mouse.setXY(event.clientX, event.clientY);
      draggableConfig.draggingObj = new DragElement(ele);
      draggableConfig.draggingObj
       .setXY(ele.style.left, ele.style.top)
       .setEleCss({
        "zIndex": draggableConfig.zIndex++,
        "position": "relative"
       });
     }    
     ele.onselectstart = function () {
      //防止拖拽对象内的文字被选中
      return false;
     }
     dom.on(ele, "mousedown", mouseDown);
    }
    dom.on(document, "mousemove", function (event) {
     if (draggableConfig.draggingObj) {
      var mouse = draggableConfig.mouse,
       draggingObj = draggableConfig.draggingObj;
      draggingObj.setEleCss({
       "left": parseInt(event.clientX - mouse.x + draggingObj.x) + "px",
       "top": parseInt(event.clientY - mouse.y + draggingObj.y) + "px"
      });
     }
    })
    dom.on(document, "mouseup", function (event) {
     draggableConfig.draggingObj = null;
    })
    window.Drag = Drag;
   })(window, undefined);

调用方法示例:Drag(document.getElementById("obj"));

需注意的一点是,为防止在拖拽过程中选中元素内的文字,可通过onselectstart事件处理程序返回false来解决此问题。

有效拖拽区域扩展

四、扩展:有效拖拽元素 常见的拖拽效果可能如下所示:

JS拖拽插件原理与基础实现

例如弹窗的顶部区域允许拖拽,而内容区域不可拖拽,如何实现此类效果?

首先优化拖拽元素对象,增加一个目标元素target属性,表示实际被拖拽的对象。在上述登录框示例中,target即为整个登录窗口。

实际记录坐标并进行位移操作的拖拽元素即为该目标元素,但它并非整个区域都支持拖拽。我们在HTML结构中为有效拖拽区域添加class="draggable"类名:


    

代码实现如下:

<div id="obj" class="dialog" style="position:relative;left:px">
<div class="header draggable">
拖拽的有效元素
</div>
<div class="content">
拖拽对象
</div>
</div>

随后修改Drag方法如下:


 function drag(ele) {
  var dragNode = (ele.querySelector(".draggable") || ele);
  dom.on(dragNode, "mousedown", function (event) {
   var dragElement = draggableConfig.dragElement = new DragElement(ele);

   draggableConfig.mouse.setXY(event.clientX, event.clientY);
   draggableConfig.dragElement
    .setXY(dragElement.target.style.left, dragElement.target.style.top)
    .setTargetCss({
     "zIndex": draggableConfig.zIndex++,
     "position": "relative"
    });
  }).on(dragNode, "mouseover", function () {
   dom.setCss(this, draggableStyle.dragging);
  }).on(dragNode, "mouseout", function () {
   dom.setCss(this, draggableStyle.defaults);
  });
 }

主要修改点在于绑定mousedown事件的节点变为包含draggable类的有效元素。若该元素不包含draggable类,则整个元素均为有效拖拽区域。

性能优化与总结

五、性能优化和总结 由于onmousemove事件会频繁触发,可能引发性能问题。我们可以通过setTimeout来延迟绑定onmousemove事件,改进后的move函数如下:


function move(event) {
   if (draggableConfig.dragElement) {
    var mouse = draggableConfig.mouse,
     dragElement = draggableConfig.dragElement;
    dragElement.setTargetCss({
     "left": parseInt(event.clientX - mouse.x + dragElement.x) + "px",
     "top": parseInt(event.clientY - mouse.y + dragElement.y) + "px"
    });
 
    dom.off(document, "mousemove", move);
    setTimeout(function () {
     dom.on(document, "mousemove", move);
    }, );
   }
  }

总结:

整个拖拽插件的实现相对简单,但需注意以下几点:

1、实现思路:元素拖拽位置的改变等于鼠标移动的距离,关键在于准确获取鼠标的变动量和元素的原始坐标。

2、通过setTimeout延迟加载onmousemove事件以提升性能。

jQuery插件化改造

六、jQuery插件化 将其简单封装为jQuery插件,主要涉及将相关DOM操作方法替换为jQuery方法进行操作。


 ; (function ($, window, undefined) {
  //#region 拖拽元素类
  function DragElement(node) {
 
   this.target = node;
 
   node.onselectstart = function () {
    //防止拖拽对象内的文字被选中
    return false;
   }
  }
  DragElement.prototype = {
   constructor: DragElement,
   setXY: function (x, y) {
    this.x = parseInt(x) || ;
    this.y = parseInt(y) || ;
    return this;
   },
   setTargetCss: function (css) {
    $(this.target).css(css);
    return this;
   }
  }
  //#endregion
 
  //#region 鼠标元素
  function Mouse() {
   this.x = ;
   this.y = ;
  }
  Mouse.prototype.setXY = function (x, y) {
   this.x = parseInt(x);
   this.y = parseInt(y);
  }
  //#endregion
 
  //拖拽配置
  var draggableConfig = {
   zIndex: ,
   dragElement: null,
   mouse: new Mouse()
  };
 
  var draggableStyle = {
   dragging: {
    cursor: "move"
   },
   defaults: {
    cursor: "default"
   }
  }
 
  var $document = $(document);
 
  function drag($ele) {
   var $dragNode = $ele.find(".draggable");
   $dragNode = $dragNode.length > ? $dragNode : $ele;
   
 
   $dragNode.on({
    "mousedown": function (event) {
     var dragElement = draggableConfig.dragElement = new DragElement($ele.get());
 
     draggableConfig.mouse.setXY(event.clientX, event.clientY);
     draggableConfig.dragElement
      .setXY(dragElement.target.style.left, dragElement.target.style.top)
      .setTargetCss({
       "zIndex": draggableConfig.zIndex++,
       "position": "relative"
      });
    },
    "mouseover": function () {
     $(this).css(draggableStyle.dragging);
    },
    "mouseout": function () {
     $(this).css(draggableStyle.defaults);
    }
   })
  }
 
  function move(event) {
   if (draggableConfig.dragElement) {
    var mouse = draggableConfig.mouse,
     dragElement = draggableConfig.dragElement;
    dragElement.setTargetCss({
     "left": parseInt(event.clientX - mouse.x + dragElement.x) + "px",
     "top": parseInt(event.clientY - mouse.y + dragElement.y) + "px"
    });
 
    $document.off("mousemove", move);
    setTimeout(function () {
     $document.on("mousemove", move);
    }, );
   }
  }
 
  $document.on({
   "mousemove": move,
   "mouseup": function () {
    draggableConfig.dragElement = null;
   }
  });
 
  $.fn.drag = function (options) {
   drag(this);
  }
 
 })(jQuery, window, undefined)

以上就是本文对JS拖拽插件实现步骤的详细介绍,希望对大家有所帮助。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章