一、基础拖拽
- 需求: 页面中的盒子dragObj,我们需要它有拖拽功能;
- 点击这个盒子,鼠标左键不松开
 - 按住鼠标左键不松开移动鼠标,在鼠标移动时盒子要跟着盒子移动;
 - 当我拖动到目的地(我想停止拖拽的地方)松开鼠标左键,盒子就要停在松开鼠标的位置;
 - 当我松开鼠标后,无论我怎么动鼠标,盒子都不能跟着动了
 
 - 实现思路:我们分析需求中的拖拽阶段,发现移动分为三个阶段
- 鼠标按下时赋予这个盒子可以被拖动的能力(鼠标动盒子跟着动);
 - 拖动其实就是鼠标移动,盒子跟随,即在mousemove事件中,实现鼠标跟随;
 - 松开鼠标左键,移动盒子的可以被拖动的能力
综上,在鼠标按下时,即mousedown事件触发时,给元素绑定mousemove事件;在mousemove事件中实现鼠标跟随。最后鼠标抬起即mouseup事件触发,在mouseup事件函数中取消可以被拖动的能力(在mouseup时移除mousemove事件) 
 - html 代码
 
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title><style>* {margin: 0;padding: 0;}.dragObj {position: absolute;width: 200px;height: 200px;background: red;}.box {width: 300px;height: 300px;background: #00b38a;}</style></head><body><div class="dragObj" id="dragObj" style="left: 100px; top: 100px"></div><div class="box" id="box"></div><script src="js/6-发布定阅准备.js"></script></body></html>
- JS
 
let dragObj = document.getElementById('dragObj');dragObj.onmousedown = dragStart; // 监听盒子的鼠标按下事件,在事件函数中赋予盒子可以被拖动的能力;dragObj.onmouseup = dragEnd; // 鼠标抬起结束拖拽function dragStart(e) {// 开始拖拽// 1. 记录盒子初始位置、鼠标按下的位置this.startL = parseFloat(this.style.left);this.startT = parseFloat(this.style.top);// 2. 记录鼠标按下时的鼠标位置this.startX = e.pageX;this.startY = e.pageY;// 3. 赋予元素可以被拖拽的能力dragObj.onmousemove = dragMove;}function dragMove(e) {// 拖动:在拖动过程中不断的计算鼠标现在所处的位置相对于鼠标按下的位置移动的距离,然后加上盒子的初始位置,就是盒子应该出现的位置// 1. 计算当前鼠标位置相对于鼠标按下移动的距离let moveX = e.pageX - this.startX;let moveY = e.pageY - this.startY;// 2. 计算盒子应该出现的位置let curL = this.startL + moveX;let curT = this.startT + moveY;// 3. 将盒子的left和top分别设置为它应该出现的值this.style.left = `${curL}px`;this.style.top = `${curT}px`;}function dragEnd() {dragObj.onmousemove = null;}
二、解决鼠标丢失
鼠标丢失的原因:
当鼠标移动的时候,因为浏览器计算盒子应该出现的位置是需要一定时间的。如果在计算的这一段时间内再次移动鼠标,因为上一次mousemove时盒子的位置还没计算出来,所以盒子没办法去到该去的位置,这时鼠标又去了一个新位置,所以盒子就更跟不上了,所以就出现了鼠标把盒子丢失了的现象;丢失元素后,即便鼠标再移动,但是不是在元素上移动的了,所以无法触发元素的onmousemove事件了,所以元素也就不会再追随鼠标了;
丢失元素后再抬起鼠标左键,触发的也不是盒子的mouseup事件,所以盒子的跟随鼠标移动的能力也没能被移除,这就导致当鼠标再次回到盒子上时,盒子还能跟着动;
解决方案:
- 将元素和鼠标绑定在一起 setCapture(),当拖拽结束后再解绑 releaseCapture() 【Chrome不兼容、IE 和 ff可以用】
 - 因为鼠标不管怎么动,都出不了浏览器页面,所以我们把元素的mousemove和mouseup事件绑定给document;(采用事件委托的思想解决问题)
 
let dragObj = document.getElementById('dragObj');dragObj.onmousedown = dragStart;dragObj.onmouseup = dragEnd;function dragStart(e) {// this.setCapture();this.startX = e.pageX;this.startY = e.pageY;this.startL = parseFloat(this.style.left);this.startT = parseFloat(this.style.top);document.onmousemove = dragMove.bind(this);}function dragMove(e) {let moveX = e.pageX - this.startX;let moveY = e.pageY - this.startY;let curL = this.startL + moveX;let curT = this.startT + moveY;this.style.left = curL + 'px';this.style.top = curT + 'px';}function dragEnd(e) {// this.releaseCapture()document.onmousemove = null;}
三、原生拖放
HTML5原生支持拖拽,但是需要给被拖动元素设置其行内属性 draggable = true;
- HTML代码:
 
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title><style>#litBox {position: absolute;width: 100px;height: 100px;background: red;}#bigBox {width: 300px;height: 300px;border: 1px solid #000;margin: 50px auto;}#fileBox {margin: 30px auto;width: 300px;height: 300px;border: 3px dashed #00b38a;border-radius: 5px;text-align: center;}#fileBox:before {content: '+';font-size: 200px;text-align: center;line-height: 250px;color: #333333;}</style></head><body><div class="fileBox" id="fileBox"></div><div id="litBox" draggable="true"></div><div id="bigBox"></div><script src="js/5-dataTransfer对象.js"></script></body></html>
- 原生拖拽实现也是基于原生的拖放事件
 
let litBox = document.getElementById('litBox');let bigBox = document.getElementById('bigBox');
ondragstart 开始拖拽:按下鼠标并移动鼠标就会触发
litBox.ondragstart = function (e) {console.log('start');e.dataTransfer.setData('Text', this.id);};
ondrag 拖动的过程中触发
litBox.ondrag = function () {// 拖动的过程中触发console.log('dragging')};
ondragend 拖拽结束触发
litBox.ondragend = function () {console.log('end')};
- 以上三个都是相对于被拖动的元素;
 - 以下事件都是相对于放置元素的:
 
ondragover 当被拖拽的元素经过bigBox时会触发bigBox的ondragover事件
bigBox.ondragover = function (e) {// 当被拖拽的元素经过bigBox时会触发bigBox的ondragover事件console.log('over');this.style.backgroundColor = 'blue';e.preventDefault();};
ondragleave 当被拖拽的元素经过离开bigBox时触发
bigBox.ondragleave = function () {// 当被拖拽的元素经过离开bigBox时触发this.style.backgroundColor = '';console.log('leave')};
ondrop 当被拖拽的元素在bigBox上松开鼠标时触发
bigBox.ondrop = function (e) {// 当被拖拽的元素在bigBox上松开鼠标时触发console.log('drop');console.log(e.dataTransfer.getData('Text'))let id = e.dataTransfer.getData('Text');bigBox.appendChild(document.getElementById(id))};
四、dataTransfer对象
在原生的拖拽事件中,可以自定义存储数据;
在拖拽的事件对象中有一个 dataTransfer对象,基于这个对象可以自定义存储或者读取数据;
存储:
- dataTransfer.setData(‘key’, ‘value’)
 
获取:
- dataTransfer.getData(‘key’)
 
let litBox = document.getElementById('litBox');let bigBox = document.getElementById('bigBox');litBox.ondragstart = function (e) {// 在拖拽的时候需要设置元素ide.dataTransfer.setData('id', this.id);};bigBox.ondragover = function (e) {// 在drageover中阻止默认行为,否则无法触发ondraop事件e.preventDefault();};bigBox.ondrop = function (e) {let id = e.dataTransfer.getData('id'); // 通过e.dataTransfer对象的getData获取被拖拽元素的idconsole.log(id)};// 此外,如果是拖拽文件到放置目标元素中,在 dataTransfer.files 中存储了这些文件的信息;let fileBox = document.getElementById('fileBox');fileBox.ondragover = function (e) {e.preventDefault();};fileBox.ondrop = function (e) {console.log(e.dataTransfer.files);// name: 带拓展名文件名// size: 文件大小// type: 'text/plain'e.preventDefault();};
五、原生拖放示例
- 将页面中的小盒子litBox拖动到大盒子中
 
// 原生拖拽实现也是基于原生的拖放事件let litBox = document.getElementById('litBox');let bigBox = document.getElementById('bigBox');litBox.ondragstart = function (e) {// 在拖拽的时候需要设置元素ide.dataTransfer.setData('id', this.id);};bigBox.ondragover = function (e) {// 在drageover中阻止默认行为,否则无法触发ondraop事件e.preventDefault();};bigBox.ondrop = function (e) {let id = e.dataTransfer.getData('id'); // 通过e.dataTransfer对象的getData获取被拖拽元素的idlet ele = document.getElementById(id);this.appendChild(ele);e.preventDefault();};
六、发布订阅准备
let box = document.getElementById('box');/*box.onclick = function () {console.log(1)};box.onclick = function () {console.log(2)};*/function fn1() {console.log(1)}function fn2() {console.log(2)}function fn3() {console.log(3)}box.addEventListener('click', fn1, false);box.addEventListener('click', fn2, false);box.addEventListener('click', fn3, false);box.addEventListener('click', fn3, false); // 同一个函数不能再同一个阶段、同一事件类型重复绑定
- DOM0级事件是给元素对象的事件属性赋值,但是一个属性只能存储一个值,多次绑定这个属性存储的就是最后一个函数;
 - DOM2: DOM2级事件的每一个事件类型都有一个事件池(类似数组的一个东西),我们次次addEventListener就是向这个事件池中添加一个方法,添加完并不会立即执行,而是等到触发这个事件的时候才会真正的执行,而且是按照我们绑定的顺序执行;
 - 事件:元素或浏览器窗口发生的特殊的交互瞬间;事件是一个时刻,所以广义来讲,在js中所有的时刻都可以成为事件,如5s后,动画停止后;
 - 而事件思想就是预约这个时刻,提前准备好事件函数,当事件时刻发生时就把所有预约这个时刻的所有函数执行了。
 - 原生事件都是在点击等操作后,浏览器触发而执行事件监听函数;而其他广义事件,我们自定义事件池,同时设置向事件池中增加事件函数以及移除事件函数的额方法,最后在时刻到来时,我们手动执行方法,这种广义事件思想称为发布订阅模式;
 
七、发布订阅
发布订阅模式:是模拟DOM2级事件的事件池思想,在某一个时刻到来时,我们要做很多的事情(很多函数)。我们准备一个数组当做一个事件池,并且提供向事件池中加入函数的方法以及移除的方法,当时刻来临时,我们把事件池中的方法取出来挨个执行;
发布订阅:
- 订阅:订阅该时刻到来,把想做的事情加入事件池
 - 发布:时刻真的到来了,把事件池中的方法都执行了
 
// 准备事件池:let ary = [];function addListener(fn) {if (ary.includes(fn)) return;ary.push(fn)}function removeListener(fn) {// 数组.filter 方法,把数组中满足条件的(回调函数返回true的项)组成一个新数组,原数组不变;ary = ary.filter(item => item !== fn);}function fire() {ary.forEach(item => item())}function fn1(_this) {console.log(1)}function fn2() {console.log(3)}function fn3() {console.log(3)}// 订阅5s后的这个时刻add(fn1);add(fn2);add(fn3);// 取消订阅removeListener(fn3);setTimeout(function () {// 5s后时刻来临,就把事件池中的方法都执行了fire(this);}, 5000);
八、封装发布订阅
class Subscribe {constructor () {this.pond = [];}includes (fn) {// 判断当前事件池是否包含某一个函数return this.pond.includes(fn);}addListener (fn) {// 不能重复添加if (!this.includes(fn)) this.pond.push(fn);return this;}removeListener (fn) {// 取消订阅if (this.includes(fn)) {this.pond = this.pond.filter(item => item !== fn);}return this;}fire (...args) {// 等到时刻到来时把事件池中的的函数都执行了this.pond.forEach(item => item(...args));}}function fn1() {console.log(1)}function fn2() {console.log(2)}function fn3() {console.log(3)}let plan = new Subscribe();plan.addListener(fn1).addListener(fn2).addListener(fn3);setTimeout(() => plan.fire([1, 2, 3]), 5000);
【发上等愿,结中等缘,享下等福,择高处立,寻平处住,向宽处行】
