Всем привет, я Шэнь Юань. Сегодня давайте создадим гаджет и инкапсулируем плагин анимации с помощью собственного JS. Эффект следующий:
1. Анализ спроса
Инкапсулировать плагин, передать в качестве параметра DOM-объект маленького шарика, чтобы маленький шарик мог двигаться после нажатия и отпускания мыши, и совершал линейное движение с равномерным замедлением в горизонтальном направлении.Начальная скорость это скорость, когда мышь перемещается.Движение в прямом направлении подобно свободному падению. Более того, маленький шарик всегда движется, не покидая границы браузера, и при попадании в границу возникает эффект отскока, как показано на рисунке.
Во-вторых, идеи кардинга
Чтобы проанализировать такой процесс, он примерно пройдет следующие ключевые этапы:
- 1. Когда мышь нажата, запишите информацию о начальном положении мяча.
- 2. Сдвиньте мышь после нажатия на нее и запишите скорость движения в момент отпускания мыши.
- 3. После того, как мышь будет отпущена, в горизонтальном направлении позвольте шарику совершить равномерное замедление в соответствии с только что записанной скоростью движения и установите вертикальное ускорение вниз в вертикальном направлении, чтобы начать движение.
- 4. Когда горизонтальная скорость уменьшается до 0, горизонтальное движение прекращается, когда вертикальная скорость уменьшается до 0 или становится достаточно малой, вертикальное движение прекращается.
3. Анализ сложности
Видя это, предполагается, что ваше мышление стало намного яснее, но все еще могут быть некоторые проблемы, которые труднее решить.
Во-первых, как вы получаете скорость мяча, когда отпускаете его? Как выразить эффект этого ускорения?
С точки зрения реализации это очень важный вопрос. Однако на самом деле это очень просто.
У самого браузера есть время отклика.Можно использовать его как камеру.После привязки события к элементу DOM каждый временной интервал (обычно очень короткий,зависит от разных производителей браузеров и производительности компьютера,здесь я Использование хрома,консервативная оценка 20 мс) сделает снимок элемента и запишет его состояние. В процессе перетаскивания после нажатия кнопки мыши фактически делается бесконечное количество снимков элемента. Если я теперь буду каждый раз записывать разницу положения между текущим фото и предыдущим фото, можно ли использовать разницу в положении мяча между последним фото и предпоследним фото как мгновенную скорость вылета? Конечно. в погоню, выложи фото:
Точно так же, чтобы добиться эффекта ускорения, сначала проясните вопрос, что такое скорость? Скорость — это расстояние, пройденное за единицу времени.Здесь это расстояние в пределах 20 мс.Затем каждый раз, когда я делаю снимок, я увеличиваю или уменьшаю это расстояние на значение, и это значение является ускорением.
4. Начальная реализация
Когда большая часть проблем была рассмотрена, теперь приступайте к осознанию. Первый — это базовый стиль, который относительно прост.
<!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;
В этот момент проявляется полный эффект, вы можете скопировать и испытать его на себе.
4. Примите публикацию-подписку
Предполагается, что после прочтения этого кода вы также поймете, что реализовать эту функцию очень просто. Но на самом деле, как стандарт плагина, этот код имеет некоторые потенциальные проблемы, причем не логические, а дизайнерские. Грубо говоря, на самом деле его масштабируемость невелика, если я хочу перенастроить эффект или переписать эффект напрямую, мне нужно искать и модифицировать его в этом тяжелом коде.
Поэтому наша цель здесь не просто предоставить функцию, это не просто игрушка, мы должны думать о том, как сделать ее более универсальной и максимально использовать повторно. Здесь я хотел бы процитировать часть O принципов проектирования SOLID, которые хорошо известны в области разработки программного обеспечения — принцип открытости-закрытости.
开放封闭原则主要体现在两个方面:
对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。
Мы хотим сделать как можно меньше изменений в самом классе, потому что вы не можете предсказать, как изменится конкретная функциональность.
Итак, как решить эту проблему? Очень просто, открыто для расширения, мы предоставим конкретный код эффекта в форме расширения и расширим класс вместо того, чтобы помещать все это в класс. Наш особый подход заключается в использовании модели публикации-подписки.
发布—订阅模式又叫观察者模式,它定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
В качестве примера возьмем только что реализованную функцию.Когда объект создан, я открываю пул и помещаю методы, которые должны быть выполнены в этот пул.При нажатии мыши я беру функции из пула и последовательно их выполняю. Для мышки Если отпустите, создайте еще один пул.Так же это и публикация-подписка.
В 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>
//原生JS 小技巧:
//直接写box跟document.getElementById('box')是一样的效果
let drag = new Drag(box);
extendDrag(drag);
</script>
Далее вы снова можете увидеть этот живой мяч.
6. Узелковый (чуйский) язык (ню)
Поздравляю, дочитать до этого места непросто. Сначала проголосуйте за вас!
Здесь я не просто говорю о реализации эффекта и вставке кода, но я познакомлю вас со всем процессом инкапсуляции плагина. В сценарии «публикация-подписка» легче понять эту идею дизайна. На самом деле, вы можете видеть, что в этом процессе добавляется не так много функций, но эта волна операций действительно того стоит, потому что она делает весь код более гибким. Оглядываясь назад, например, на механизм пула событий DOM2, хук жизненного цикла Vue и т. д., вы поймете, почему они так устроены. В принципе, он ничем не отличается от этого пакета. Когда вы думаете об этом, многое становится яснее.
На мой взгляд, независимо от того, какую сторону работы по разработке вы выполняете, на самом деле большинство бизнес-сценариев и наиболее популярных фреймворков, скорее всего, исчезнут через несколько лет, но что действительно останется с вами на всю жизнь, так это программирование. В моем понимании смысл программирования гораздо больше, чем просто создание колес и написание плагинов, чтобы выделиться.Вы можете использовать свое суждение, чтобы направлять и оптимизировать свое следующее решение, а не прыгать на технологической волне быстрой итерации и становится все более тревожным. Мне кажется, это то, к чему должен стремиться программист.