你們好,我是神三元。 今天咱們來作一個小玩意,用原生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的生命週期鉤子等等,你就會明白它們爲何要這麼設計,原理上和此次封裝沒有區別,這樣一想,不少東西就更加清楚了。
在我看來,不管你是作哪一個端的開發工做,其實大部分業務場景、大部分流行的框架技術都極可能會在若干年後隨風而逝,但真正留下來的、伴隨你一輩子的東西是編程思想。在個人理解中,編程的意義遠不止造輪子,寫插件,來顯得本身金玉其外,而是留心思考,提煉出一些思考問題的方式,從而在某個肯定的時間點讓你擁有極其敏銳的判斷,來指導和優化你下一步的決策,而不是縱身於飛速迭代的技術浪潮,日漸焦慮。我以爲這是一個程序員應該追求的東西。