拖拽类,一段代码的进化史

最开始学习面向对象编写代码的时候,自己是个菜鸡,2018年了,还是个菜鸡,废话不多说。当年面向对象写法的第一个示例就是实现一个拖拽的类的编写,使用的是构造函数的prototype属性,为实例对象提供方法。最近的工作也是和拖拽类打交道,这段代码也逐渐的进化并应用到多个使用场景,也从prototype的写法进化为ES6 class。

实现拖拽的原理十分简单相信大家也都是烂熟于心,核心就是元素的初始坐标,和鼠标移动终止位置的坐标差值,其中要去除点击位置到元素的左、上边界。

es5 prototype的写法大概是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function Drag(id){
var _this=this;
this.disx=0;
this.disy=0;
this.oDiv=document.getElementById(id);
this.oDiv.onmousedown=function(ev){
_this.fnDown(ev);
// 阻止冒泡
return false;
}
};
// 点击
Drag.prototype.fnDown=function (ev){
var _this=this;
// 兼容IE
var oev=ev||event;
// 记录点击位置到元素上边和左边的距离
this.disx=oev.clientX-this.oDiv.offsetLeft;
this.disy=oev.clientY-this.oDiv.offsetTop;
document.onmousemove=function(ev){
_this.fnMove(ev);
};
document.onmouseup=function(ev){
_this.fnUp(ev);
};
};
// 移动
Drag.prototype.fnMove=function (ev){
var oev=ev||event;
// 计算坐标的差值
this.oDiv.style.left=oev.clientX-this.disx+"px";
this.oDiv.style.top=oev.clientY-this.disy+"px";
};
// 销毁绑定事件
Drag.prototype.fnUp=function (){
document.onmousemove=null;
document.onmouseup=null;
};

有几个注意点

  • 鼠标mousemove,moveuseup事件是在点击事件之后绑定的,鼠标抬起后要销毁事件,不然这两个时间一直存在,导致元素跟着鼠标走。
  • mousemove,moveuseup事件的绑定最好绑定在windowdocument上,因为直接绑定在拖拽元素上会出现,鼠标太快超出元素大小,停止移动的现象。
  • 这里的事件绑定使用的是dom1级事件,直接给属性赋值,老代码了不推荐。

现在肯定是要用ES6 class来实现,代码更清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Drag {
constructor(el) {
this.el = el
// 拖拽信息
this.mouse = {}
this.mouse.init = false
this.init()
this.initDrag()
}

//绝对定位初始化
init() {
this.el.style.position = 'absolute'
this.el.style.top = `${this.el.offsetTop}px`
this.el.style.left = `${this.el.offsetLeft}px`
}

// 拖动初始化
initDrag() {
this.el.addEventListener('mousedown', e => {
if (/input|textarea/.test(e.target.tagName.toLowerCase())) return
this.mouse.init = true
this.mouse.offsetX = e.pageX - this.el.offsetLeft
this.mouse.offsetY = e.pageY - this.el.offsetTop
// 建立一个函数引用,进行销毁
this.moveHandler = this.move.bind(this)
this.upHanler = this.up.bind(this)
window.addEventListener('mousemove', this.moveHandler)
window.addEventListener('mouseup', this.upHanler)
})
}
// 拖动
move(e) {
if (!this.mouse.init) {
return
}
this.el.style.left = e.pageX - this.mouse.offsetX + 'px'
this.el.style.top = e.pageY - this.mouse.offsetY + 'px'
}
// 松开
up() {
this.mouse.init = false
console.log('ok')
window.removeEventListener('mousemove', this.moveHandler)
window.removeEventListener('mouseup', this.upHanler)
}
}

和老代码相比有几个升级优化的部分

  • 主要给元素添加position:absolute,初始化他的位置,更合理。
  • 使用了e.pageX,e.pageY,获取元素相对视口的位置可用getBoundingClientRect
    image1
  • 当元素为input或者textarea时不能拖动。
  • 使用dom2级事件进行事件监听和接触监听

注意
在class内默认严格模式,一定要主要上下文的this指向,直接给window绑定一个方法,例如window.addEventListener('mousemove', this.move)此时的监听函数的this是指向window的,这显然无法实现拖动,所以要this.move.bind(this)绑定到实例本身。
我为什么要建立一个函数引用呢?

1
2
3
// 建立一个函数引用,进行销毁
this.moveHandler = this.move.bind(this)
this.upHanler = this.up.bind(this)

原因是因为每调用一次Function.bind就会创建一个新的函数,直接调用
window.removeEventListener('mousemove', this.move.bind(this))
是无法销毁你监听事件的,因为这已经是两个函数了,只是内容一样而已。

1
2
3
4
5
6
function a () {console.log(1)}

let b = a.bind(null)
let c = a.bind(null)
b == a //false
c == b //false

ES5中,坚持一个原则:this永远指向最后调用它的那个对象!!!
ES6中,箭头函数没有this,它会向父级查找离它最近的一个非箭头函数的this,找不到就是undefined
普通函数的this会指向window,严格模式下指向undefined

有几种改变this的方法

  • new 方法
  • 箭头函数
  • apply,call,bind

关于this不在一一赘述了,网上大神比我讲的好。下面说下,拖拽类的使用场景:

  • 实现一个vue拖拽指令v-drag,简单易用适合不复杂场景

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    export default {
    name: 'drag',
    bind: function(el) {
    var offsetX = 0;
    var offsetY = 0;

    function move(e) {
    el.style.left = e.pageX - offsetX + 'px';
    el.style.top = e.pageY - offsetY + 'px';
    }

    function up() {
    window.removeEventListener('mousemove', move);
    window.removeEventListener('mouseup', up);
    }

    function down(e) {
    if (/input|textarea/.test(e.target.tagName.toLowerCase())) return;

    offsetX = e.pageX - el.offsetLeft;
    offsetY = e.pageY - el.offsetTop;
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', up);
    }

    el.addEventListener('mousedown', down)
    }
    }
  • 结合iscroll5实现拖拽滚动,better-scroll应该也可以

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    handleMouseMove(e) {
    if (!this.mouse.init) {
    return
    }
    const deltaX = this.mouse.startX - e.pageX
    const deltaY = this.mouse.startY - e.pageY
    this.mouse.cord = [deltaX > 0, deltaY > 0]
    this.myScroll.scrollTo(this.mouse.scrollerX - deltaX, this.mouse.scrollerY - deltaY)
    },
    handleMouseDown(e) {
    // 特殊区域处理
    const content = this.$refs.scroller.$el
    if (!e.target.parentNode.contains(content)) return
    if (e.target.contains(content)) return
    if (e.target && e.target.nodeName === 'CANVAS') return
    if (!this.myScroll) return
    this.mouse.init = true
    this.direction = true
    this.mouse.startX = e.pageX
    this.mouse.startY = e.pageY
    this.mouse.scrollerX = this.myScroll.x
    this.mouse.scrollerY = this.myScroll.y
    },
    handleMouseUp(e) {
    const content = this.$refs.scroller.$el
    if (!e.target.parentNode.contains(content)) return
    if (e.target.contains(content)) return
    this.mouse.init = false
    this.direction = false
    let deltaX = this.myScroll.x - START_X
    let deltaY = this.myScroll.y - START_Y
    this.saveScrollerConfig({
    x: deltaX,
    y: deltaY
    })
    }
  • 拖拽进度条等等。

从ES5到ES6,从prototype到class,一段代码的进化史。

HTML5拖放drag,drop待续
欢迎在GitHub给我留言,一起学习,一起进步。