最开始学习面向对象编写代码的时候,自己是个菜鸡,2018 年了,还是个菜鸡,废话不多说。当年面向对象写法的第一个示例就是实现一个拖拽的类的编写,使用的是构造函数的 prototype 属性,为实例对象提供方法。最近的工作也是和拖拽类打交道,这段代码也逐渐的进化并应用到多个使用场景,也从 prototype 的写法进化为 ES6 class。
实现拖拽的原理十分简单相信大家也都是烂熟于心,核心就是元素的初始坐标,和鼠标移动终止位置的坐标差值,其中要去除点击位置到元素的左、上边界。
es5 prototype 的写法大概是这样的
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事件的绑定最好绑定在window或document上,因为直接绑定在拖拽元素上会出现,鼠标太快超出元素大小,停止移动的现象。- 这里的事件绑定使用的是 dom1 级事件,直接给属性赋值,老代码了不推荐。
现在肯定是要用 ES6 class 来实现,代码更清晰:
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
- 当元素为
input或者textarea时不能拖动。 - 使用 dom2 级事件进行事件监听和接触监听
注意
在 class 内默认严格模式,一定要主要上下文的this指向,直接给window绑定一个方法,例如window.addEventListener('mousemove', this.move)此时的监听函数的this是指向window的,这显然无法实现拖动,所以要this.move.bind(this)绑定到实例本身。
我为什么要建立一个函数引用呢?
// 建立一个函数引用,进行销毁
this.moveHandler = this.move.bind(this);
this.upHanler = this.up.bind(this);
原因是因为每调用一次Function.bind就会创建一个新的函数,直接调用
window.removeEventListener('mousemove', this.move.bind(this))
是无法销毁你监听事件的,因为这已经是两个函数了,只是内容一样而已。
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,简单易用适合不复杂场景
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应该也可以
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给我留言,一起学习,一起进步。