你们好,我是神三元。 今天咱们来作一个小玩意,用原生JS封装一个动画插件。效果以下:javascript
封装一个插件,将小球的DOM对象做为参数传入,使得小球在鼠标按下和放开后可以运动,在水平方向作匀减速直线运动,初速度为鼠标移开瞬间的速度,在竖直方向的运动相似于自由落体运动。而且,小球的始终在不离开浏览器的边界运动,碰到边界会有如图的反弹效果。css
分析这样的一个过程,其中大体会经历一下的关键步骤:html
看到这里,估计你的思路清晰了很多,但可能仍是有一些比较难以搞定的问题。vue
首先,你怎么拿到松开手瞬间的小球移动速度?如何去表达出这个加速度的效果?java
在实现方面,这是很是重要的问题。不过,其实很是的简单。程序员
浏览器自己就是存在反应时间的,你能够把它当作一个摄像机,在给DOM元素绑定了事件以后,每隔一段时间(通常很是的短,根据不一样浏览器厂商和电脑性能而定,这里我用到chrome,保守估计为20ms)会给这个元素拍张照,记录它的状态。在按下鼠标以后的拖动过程当中,事实上会给元素拍摄无数张照片。若是如今每通过一段时间,我记录当下当前照片与上一段照片的位置差,那么最后一次拍照和倒数第二次拍照的小球位置差距,是否是就能够做为离开的瞬时速度呢?固然能够啦。废话很少说,上图:chrome
一样,对实现加速度的效果,首先弄清一个问题,什么是速度?速度就是单位时间内运动的距离,这里暂且把它当作20ms内的距离,那么我每次拍照时,将这个距离增长或减小一个值,这个值就是加速度。编程
当大部分问题考虑清楚以后,如今开始实现。 首先是基本的样式,比较简单。数组
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>狂奔的小球</title>
<link rel="stylesheet" href="css/reset.min.css">
<style> html, body { height: 100%; overflow: hidden; } #box{ position: absolute; top: 100px; left: 100px; width: 150px; height: 150px; border-radius: 50%; background: lightcoral; cursor: move; z-index: 0; } </style>
</head>
<body>
<div id="box"></div>
</body>
</html>
复制代码
如今来完成核心的JS代码,采用ES6语法浏览器
//drag.js
class Drag {
//ele为传入的DOM对象
constructor(ele) {
//初始化参数
this.ele = ele;
['strX', 'strY', 'strL', 'strT', 'curL', 'curT'].forEach(item => {
this[item] = null;
});
//为按下鼠标绑定事件,事件函数必定要绑定this,在封装过程当中this统一指定为实例对象,下不赘述
this.DOWN = this.down.bind(this);
this.ele.addEventListener('mousedown', this.DOWN);
}
down(ev) {
let ele = this.ele;
this.strX = ev.clientX;//鼠标点击处到浏览器窗口最左边的距离
this.strY = ev.clientY;//鼠标点击处到浏览器窗口最上边的距离
this.strL = ele.offsetLeft;//元素到浏览器窗口最左边的距离
this.strT = ele.offsetTop;//元素到浏览器窗口最上边的距离
this.MOVE = this.move.bind(this);
this.UP = this.up.bind(this);
document.addEventListener('mousemove', this.MOVE);
document.addEventListener('mouseup', this.UP);
//flag
//清理上一次点击造成的一些定时器和变量
clearInterval(this.flyTimer);
this.speedFly = undefined;
clearInterval(this.dropTimer);
}
move(ev) {
let ele = this.ele;
this.curL = ev.clientX - this.strX + this.strL;
this.curT = ev.clientY - this.strY + this.strT;
ele.style.left = this.curL + 'px';
ele.style.top = this.curT + 'px';
//flag
//功能: 记录松手瞬间小球的速度
if (!this.lastFly) {
this.lastFly = ele.offsetLeft;
this.speedFly = 0;
return;
}
this.speedFly = ele.offsetLeft - this.lastFly;
this.lastFly = ele.offsetLeft;
}
up(ev) {
//给前两个事件解绑
document.removeEventListener('mousemove', this.MOVE);
document.removeEventListener('mouseup', this.UP);
//flag
//水平方向
this.horizen.call(this);
this.vertical.call(this);
}
//水平方向的运动
horizen() {
let minL = 0,
maxL = document.documentElement.clientWidth - this.ele.offsetWidth;
let speed = this.speedFly;
speed = Math.abs(speed);
this.flyTimer = setInterval(() => {
speed *= .98;
Math.abs(speed) <= 0.1 ? clearInterval(this.flyTimer):null;
//小球当前到视口最左端的距离
let curT = this.ele.offsetLeft;
curT += speed;
//小球到达视口最右端,反弹
if (curT >= maxL) {
this.ele.style.left = maxL + 'px';
speed *= -1;
return;
}
//小球到达视口最右端,反弹
if (curT <= minL) {
this.ele.style.left = minL + 'px';
speed *= -1;
return;
}
this.ele.style.left = curT + 'px';
}, 20);
}
//竖直方向的运动
vertical() {
let speed = 9.8,
minT = 0,
maxT = document.documentElement.clientHeight - this.ele.offsetHeight,
flag = 0;
this.dropTimer = setInterval(() => {
speed += 10;
speed *= .98;
Math.abs(speed) <= 0.1 ? clearInterval(this.dropTimer):null
//小球当前到视口最左端的距离
let curT = this.ele.offsetTop;
curT += speed;
//小球飞到视口顶部,反弹
if (curT >= maxT) {
this.ele.style.top = maxT + 'px';
speed *= -1;
return;
}
//小球落在视口底部,反弹
if (curT <= minT) {
this.ele.style.top = minT + 'px';
speed *= -1;
return;
}
this.ele.style.top = curT + 'px';
}, 20);
}
}
window.Drag = Drag;
复制代码
到此,完整的效果就出来了,你能够本身复制体验一下。
估计读完这段代码,你也体会到了这个功能的实现是很是容易实现的。可是实际上,做为一个插件的标准来说,这段代码是存在一些潜在的问题的,这些问题并非逻辑上的问题,而是设计问题。直白一点说,实际上是它的扩展性不强,假若我要对某一个效果进行从新调整或者直接重写效果,我须要再这繁重的代码里面去搜索和修改。
所以,咱们这里的目的并不仅是提供一个功能,它毫不只是一个玩具,咱们应当思考,如何将它作的更有通用性,可以获得最大程度的复用。 这里,我想引用软件工程领域耳熟能详的SOLID设计原则中的O部分————开放封闭原则。
开放封闭原则主要体如今两个方面:
对扩展开放,意味着有新的需求或变化时,能够对现有代码进行扩展,以适应新的状况。
对修改封闭,意味着类一旦设计完成,就能够独立完成其工做,而不要对类进行任何修改。
复制代码
咱们但愿尽量少地对类自己进行修改,由于你没法预测具体的功能会如何变化。
那怎么解决这个问题呢?很简单,对扩展开放,咱们就将具体的效果代码以扩展的方式提供,对类扩展,而不是所有放在类里面。 咱们的具体作法就是采用发布-订阅模式。
发布—订阅模式又叫观察者模式,它定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,全部依赖于它的对象都将获得通知。
复制代码
拿刚刚实现的功能来讲,在对象建立的时候,我就开辟一个池子,将须要执行的方法放进这个池子,当鼠标按下的时候,我把池子里面的函数拿过来依次执行,对于鼠标松开就再建立一个池子,同理,这就是发布-订阅。
jQuery里面有现成的发布订阅方法。
//开辟一个容器
let $plan = $.callBack();
//往容器里面添加函数
$plan.add(function(x, y){
console.log(x, y);
})
$plan.add(function(x, y){
console.log(y, x);
})
$plan.fire(10, 20);//会输出10,20 20,10
//$plan.remove(function)用来从容器中删除某个函数
复制代码
如今咱们不妨原生JS手写一下简单的发布-订阅,让咱们原生撸到底
//subscribe.js
class Subscribe {
constructor() {
//建立容器
this.pond = [];
}
//向容器中增长方法,注意去重
add(fn) {
let pond = this.pond,
isExist = false;
//去重环节
pond.forEach(item => item === fn ? isExist = true : null);
!isExist ? pond.push(fn) : null;
}
remove(fn) {
let pond = this.pond;
pond.forEach((item, index) => {
if(item === fn) {
//提一下我在这里遇到的坑,这里若是写item=null是无效的
//例子:let a = {name: funtion(){}};
//let b = a.name;
//这个时候操做b的值对于a的name属性是没有影响的
pond[index] = null;
}
})
}
fire(...arg) {
let pond = this.pond;
for(let i = 0; i < pond.length; i++) {
let item = pond[i];
//若是itme为空了,最好把它删除掉
if (item === null) {
pond.splice(i, 1);
//若是用了splice要防止数组塌陷问题,即删除了一个元素后,后面全部元素的索引默认都会减1
i--;
continue;
}
item(...arg);
}
}
}
window.Subscribe = Subscribe;
复制代码
//测试一下
let subscribe = new Subscribe();
let fn1 = function fn1(x, y) {
console.log(1, x, y);
};
let fn2 = function fn2() {
console.log(2);
};
let fn3 = function fn3() {
console.log(3);
subscribe.remove(fn1);
subscribe.remove(fn2);
};
let fn4 = function fn4() {
console.log(4);
};
subscribe.add(fn1);
subscribe.add(fn1);
subscribe.add(fn2);
subscribe.add(fn1);
subscribe.add(fn3);
subscribe.add(fn4);
setInterval(() => {
subscribe.fire(100, 200);
}, 1000);
复制代码
结果:
//Drag.js
if (typeof Subscribe === 'undefined') {
throw new ReferenceError('没有引入subscribe.js!');
}
class Drag {
constructor(ele) {
this.ele = ele;
['strX', 'strY', 'strL', 'strT', 'curL', 'curT'].forEach(item => {
this[item] = null;
});
this.subDown = new Subscribe;
this.subMove = new Subscribe;
this.subUp = new Subscribe;
//=>DRAG-START
this.DOWN = this.down.bind(this);
this.ele.addEventListener('mousedown', this.DOWN);
}
down(ev) {
let ele = this.ele;
this.strX = ev.clientX;
this.strY = ev.clientY;
this.strL = ele.offsetLeft;
this.strT = ele.offsetTop;
this.MOVE = this.move.bind(this);
this.UP = this.up.bind(this);
document.addEventListener('mousemove', this.MOVE);
document.addEventListener('mouseup', this.UP);
this.subDown.fire(ele, ev);
}
move(ev) {
let ele = this.ele;
this.curL = ev.clientX - this.strX + this.strL;
this.curT = ev.clientY - this.strY + this.strT;
ele.style.left = this.curL + 'px';
ele.style.top = this.curT + 'px';
this.subMove.fire(ele, ev);
}
up(ev) {
document.removeEventListener('mousemove', this.MOVE);
document.removeEventListener('mouseup', this.UP);
this.subUp.fire(this.ele, ev);
}
}
window.Drag = Drag;
复制代码
//dragExtend.js
function extendDrag(drag) {
//鼠标按下
let stopAnimate = function stopAnimate(curEle) {
clearInterval(curEle.flyTimer);
curEle.speedFly = undefined;
clearInterval(curEle.dropTimer);
};
//鼠标移动
let computedFly = function computedFly(curEle) {
if (!curEle.lastFly) {
curEle.lastFly = curEle.offsetLeft;
curEle.speedFly = 0;
return;
}
curEle.speedFly = curEle.offsetLeft - curEle.lastFly;
curEle.lastFly = curEle.offsetLeft;
};
//水平方向的运动
let animateFly = function animateFly(curEle) {
let minL = 0,
maxL = document.documentElement.clientWidth - curEle.offsetWidth,
speed = curEle.speedFly;
curEle.flyTimer = setInterval(() => {
speed *= .98;
Math.abs(speed) <= 0.1 ? clearInterval(animateFly):null;
let curT = curEle.offsetLeft;
curT += speed;
if (curT >= maxL) {
curEle.style.left = maxL + 'px';
speed *= -1;
return;
}
if (curT <= minL) {
curEle.style.left = minL + 'px';
speed *= -1;
return;
}
curEle.style.left = curT + 'px';
}, 20);
};
//竖直方向的运动
let animateDrop = function animateDrop(curEle) {
let speed = 9.8,
minT = 0,
maxT = document.documentElement.clientHeight - curEle.offsetHeight;
curEle.dropTimer = setInterval(() => {
speed += 10;
speed *= .98;
Math.abs(speed) <= 0.1 ? clearInterval(animateFly):null;
let curT = curEle.offsetTop;
curT += speed;
if (curT >= maxT) {
curEle.style.top = maxT + 'px';
speed *= -1;
return;
}
if (curT <= minT) {
curEle.style.top = minT + 'px';
speed *= -1;
return;
}
curEle.style.top = curT + 'px';
}, 20);
};
drag.subDown.add(stopAnimate);
drag.subMove.add(computedFly);
drag.subUp.add(animateFly);
drag.subUp.add(animateDrop);
};
复制代码
在html文件中加入以下script
<script> //原生JS 小技巧: //直接写box跟document.getElementById('box')是同样的效果 let drag = new Drag(box); extendDrag(drag); </script>
复制代码
接下来,你就能从新看到那个活泼的小球啦。
恭喜你,读到了这里,至关不容易啊。先为你点个赞!
在这里我并非简单讲讲效果的实现、贴贴代码就过去了,而是带你体验了封装插件的整个过程。有了发布-订阅的场景,理解这个设计思想就更加容易了。其实你看在这个过程当中,功能并无添加多少,可是这波操做确实值得,由于它让整个代码更加的灵活。回过头看,好比DOM2的事件池机制,vue的生命周期钩子等等,你就会明白它们为何要这么设计,原理上和此次封装没有区别,这样一想,不少东西就更加清楚了。
在我看来,不管你是作哪一个端的开发工做,其实大部分业务场景、大部分流行的框架技术都极可能会在若干年后随风而逝,但真正留下来的、伴随你一辈子的东西是编程思想。在个人理解中,编程的意义远不止造轮子,写插件,来显得本身金玉其外,而是留心思考,提炼出一些思考问题的方式,从而在某个肯定的时间点让你拥有极其敏锐的判断,来指导和优化你下一步的决策,而不是纵身于飞速迭代的技术浪潮,日渐焦虑。我以为这是一个程序员应该追求的东西。