Zouhaha

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


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

最开始学习面向对象编写代码的时候,自己是个菜鸡,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
}

有几个注意点

现在肯定是要用 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)
  }
}

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

注意
在 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的方法

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

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)
  },
}
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给我留言,一起学习,一起进步。