你应该掌握的 JavaScript
高阶技能(一)之原生实现轮播图
本文内容参考 pink
老师的素材与视频讲解,特别鸣谢❤️
可能有些朋友有疑问了,轮播图制作也算高阶技能嘛?当然,轮播图对于前端朋友要求扎实的
基础
,内容涉及到js
基础、DOM
和BOM
等等内容,学会制作轮播图,能让您更进一步地掌握JavaScript
的知识,同时作为第一期,有个好开头,让大家更好上手,好啦现在开始!
轮播图在网页中是无处不在,轮播图在网页中起到非常重要的作用,我们可以发现,点击轮播图的(图示)按钮,可以切换图片,底部(图示)小矩形同样可以有切换图片的效果,还有自动切换图片的功能。对于一名前端同学,能够使用原生
实现轮播图是必备技能!
轮播图示
我们将图片进行刨析,它的原型应该长这样,如图所示:
基本流程:
可视窗口中(绿色空心矩形)只有一张图片,图片左右两侧有两个方向为左和右的按钮,点击可以进行图片切换,发生滚动效果。
页面结构要点与注意事项
1.使用元素
ul
包裹所有图片2.按钮
3.可视窗口底部位置有小圆点 与图片
一一对应
4.可视窗口
不动
,滚动区域为ul
5.需要使用动画效果
6.图片不是竖排列 而是横排列 让所有的
li
元素在一行上
tips
: 页面结构问题是 css
问题,样式不过多讲解,重点是 js
实现部分!
需求
需求一:鼠标离开轮播图,按钮消失,鼠标经过,按钮出现
需求二:点击按钮可以切换图片
需求三:图片切换,底部小圆圈一同变化,小圆圈和图片一一对应
需求四:点击底部小圆圈 ,切换到对应图片
需求五:鼠标离开,自动播放下一张图片,鼠标进入轮播图,取消自动播放下一张图片。
外部引入 js
文件,取名 index.js
完成需求一:鼠标离开轮播图,按钮消失,鼠标经过,按钮出现
- 页面加载完毕所有界面之后再执行
- 先隐藏按钮 -->
css
设置display: none
- 获取元素,设置相关事件监听。
window.addEventListener('load', function () {
//获取元素
let arrow_l = document.querySelector('.arrow-l');//左按钮
let arrow_r = document.querySelector('.arrow-l');//左按钮
let focus = document.querySelector('.focus');//可视窗口
focus.addEventListener('mouseenter',function(){
arrow_l.style.display = 'block';
arrow_r.style.display = 'block';
});
focus.addEventListener('mouseleave',function(){
arrow_l.style.display = 'none';
arrow_r.style.display = 'none';
})
})
先完成需求三 :图片切换,底部小圆圈一同变化,小圆圈和图片一一对应
- 例如 :有 n 个图片,就有 n 个小圆圈
- 小圆圈是动态生成的
- 首先得到
ul
里面图片数量(图片放在li
里面 所以就是li
的个数) - 利用循环动态生成小圆圈(放入
ol
) - 创建节点
createElement('li')
,插入节点ol.appendChild('li')
- 先删掉
ol
中所有的li
,默认ol
中第一个li
设置类名为current
,可 变为实心,表示当前选中
。
//todo... focus
//根据图片动态生成小圆圈
let ul = focus.querySelector('ul');
let ol = focus.querySelector('.circle');
for(var i = 0; i < ul.children.length; i++){
//创建li元素
//将li元素插入ol
let li = document.createElement('li');
ol.appendChild(li);
}
//ol中第一个li设置类名为current
ol.children[0].className = 'current';
//todo...something
完成小圆圈的相关操作
- 点击小圆圈,当前小圆圈为实心,其他为空心(排他思想)
排他思想
是从pink老师
所学,非常实用,干掉所有人,留下我自己。- 意思是点击当前小圆点,添加
current
类,其余的小圆圈就移除current
类。 - 生成小圆圈的同时,可以绑定点击事件监听。
//todo...
for(let i = 0; i < ul.children.length; i++){
//创建li元素 同时将li元素插入ol
let li = document.createElement('li');
ol.appendChild(li);
li.addEventListener('click',function(){
//排他思想
//点击 先清除所有li的current类名
for(let i = 0; i < ol.children.length; i++){
ol.children[i].className = '';
}
//再将被点击的li设置current类名
this.className = 'current';
});
}
//todo...
需求二 | 需求三 :点击小圆圈可以实现图片滑动效果
需要使用 animate
动画函数
动画原理
- 获取盒子当前位置
- 让盒子在当前位置加上 n 个移动距离
- 利用定时器不断重复这个操作
- 添加一个结束定时器的条件
- 此元素需要添加
定位
,才能element.style.left
生效
//html请您自行实现 只展示js部分
var div = document.querySelector('div');
var timer = setInterval(function () {
if (div.offsetLeft >= 400) {
//停止动画 本质是停止定时器
clearInterval(timer);
}
div.style.left = div.offsetLeft + 1 + 'px';
}, 30);
简单动画函数封装
需要 2个参数 【动画对象和移动到的距离】
//简单动画函数封装 obj目标对象 target目标位置
function animate(obj, target) {
var timer = setInterval(function () {
if (obj.offsetLeft >= target) {
//停止动画 本质是停止定时器
clearInterval(timer);
}
obj.style.left = obj.offsetLeft + 1 + 'px';
}, 30);
}
//如果有100个对象开启100个内存空间给变量timer
//而且不同对象都使用timer,具有歧义
改进 给不同对象添加指定不同的定时器
如果多个元素都使用这个动画函数,每次都要使用 let 或 var
声明定时器,我们可以给不同的元素使用不同的定时器,核心原理,利用 js
是一门动态语言,可以很方便的给当前对象添加属性。
function animate(obj, target) {
clearInterval(obj.timer);
obj.timer = setInterval(function () {
if (obj.offsetLeft >= target) {
//停止动画 本质是停止定时器
clearInterval(obj.timer);
// return;
} else {
obj.style.left = obj.offsetLeft + 1 + 'px';
}
}, 30);
}
//todo...
btn.addEventListener('click', function () {
animate(element, 300);
});
//如果点击次数越多 速度越快 开启太多定时器
//解决方法 让元素的定时器只有一个
缓动动画原理
需要做到轮播图的效果需要掌握缓动动画原理
缓动动画就是让元素运动速度有所变化。最常见的就是让速度慢慢停下来
思路
- 让盒子每次移动的距离逐渐变小,速度就会逐渐变小直到 0
- 核心算法:
(目标值 - 现在的位置)/ 10
作为每次移动的距离(步长) - 停止的条件:让当前盒子位置等于目标位置就停止定时器
// 缓动动画函数封装obj目标对象 target 目标位置
// 思路:
// 1. 让盒子每次移动的距离慢慢变小, 速度就会慢慢落下来。
// 2. 核心算法:(目标值 - 现在的位置) / 10 做为每次移动的距离 步长
// 3. 停止的条件是: 让当前盒子位置等于目标位置就停止定时器
function animate(obj, target) {
// 先清除以前的定时器,只保留当前的一个定时器执行
clearInterval(obj.timer);
obj.timer = setInterval(function () {
//步长值写到定时器的里面
//把我们步长值改为整数 不要出现小数的问题
// var step = Math.ceil((target - obj.offsetLeft) / 10);
let step = (target - obj.offsetLeft) / 10;
step = step > 0 ? Math.ceil(step) : Math.floor(step);
if (obj.offsetLeft == target) {
// 停止动画 本质是停止定时器
clearInterval(obj.timer);
}
// 把每次加1 这个步长值改为一个慢慢变小的值 步长公式:(目标值 - 现在的位置) / 10
obj.style.left = obj.offsetLeft + step + 'px';
}, 15);
}
// 匀速动画 就是 盒子是当前的位置 + 固定的值 10
// 缓动动画就是 盒子当前的位置 + 变化的值(目标值 - 现在的位置) / 10)
给动画函数添加回调函数
回调函数原理:函数可以作为一个参数,将这个函数作为参数传到另一个函数里面,当动画函数执行完成之后,再执行传入作为参数的函数,这个过程叫做回调
//animate.js
function animate(obj, target,callback) {
clearInterval(obj.timer);
obj.timer = setInterval(function () {
//todo...
if (obj.offsetLeft == target) {
// 停止动画 本质是停止定时器
clearInterval(obj.timer);
//回调函数写道定时器结束里面
if (callback) {
//调用函数
callback();
}
//或者写成
//callback && callback();
}
//todo...
}, 15);
}
完成 animate.js
后,引入animate.js
注意事项
(1)
index.js
依赖于animate.js
animate.js
要写到index.js
上面
(2)可视窗口不动,图片移动
ul
要设置定位,是ul
动,不是li
动,因为ul
包裹着li
元素。
(3)滚动图片的核心算法:点击某个小圆圈,图片滚动,小圆圈的
索引号
乘以图片宽度作为ul
的移动距离,比如点击第一个小圆圈,索引号为 0,若图片长度为 200 px,图片不移动,如果点击的是第二个,移动 200 px。
//todo...
li.addEventListener('click',function(){
for(let i = 0; i < ol.children.length; i++){
ol.children[i].className = '';
}
this.className = 'current';
// 点击小圆圈移动图片 移动 ul
// animate(obj,target,callback);
// target 移动距离 注意是负值
// 先拿到图片宽度
let focusWidth = focus.offsetWidth;
console.log(focusWidth);
});
//todo...
此时需要知道小圆圈的索引号,在生成小圆圈的时候,设置自定义属性
,可在点击时获取自定义属性(即为索引号)即可,然后可以使用 animate
函数
//todo...
for (let i = 0; i < ul.children.length; i++) {
//创建li元素 同时将li元素插入ol
let li = document.createElement('li');
//记录当前小圆圈的索引号,通过自定义属性
li.setAttribute('index',i);
ol.appendChild(li);
li.addEventListener('click', function () {
//排他思想
//点击 先清除所有li的current类名
for (let i = 0; i < ol.children.length; i++) {
ol.children[i].className = '';
}
//再将被点击的li设置current类名
this.className = 'current';
let index = this.getAttribute('index');
let focusWidth = focus.offsetWidth;
animate(ul,-index*focusWidth);
});
}
//todo...
现在可以实现点击小圆圈能够准确定位图片的位置的功能!
完成需求四:点击右侧按钮一次 就让图片滚动一张
可以声明一个变量 num
,点击一次,num++
,让变量乘以图片的宽度,也就是ul
滚动距离
//todo...
arrow_r.addEventListener('click',function(){
num++;
animate(ul, -num * focusWidth);
})
//todo...
现在还是有问题,会移动出图片所在区域(可视区域),而网页中轮播图都是使用图片无缝滚动原理。可以将第一张图片复制一份到最后一张,如果图片已经是最后一张了,点击会进入复制的第一张的图片,然后会“不做动画的”跳入第一张。
arrow_r.addEventListener('click', function () {
//如果走到最后复制的一张图片,此时需要将ul快速复原left改为0
if(num == ul.children.length - 1){
ul.style.left = 0;
num = 0;
}
num++;
animate(ul, -num * focusWidth);
})
改进
刚刚的做法是直接将第一张图片复制,可是真实的场景是我们不知道第一张图片具体是什么?所以让 js
帮我们复制第一张图片到最后一张。
- 克隆第一张图片,克隆
ul
第一个li
,使用cloneNode()
加true
为深克隆,复制里面的节点,false
为浅克隆。 - 添加
ul
最后一个节点,使用appendChild
//todo...
//克隆第一张图片(li)放到ul最后
let firstNode = ul.children[0].cloneNode(true);
ul.appendChild(firstNode);
//todo...
解释一下:为什么多了一个节点小圆圈个数不变 ?
因为克隆第一张图片的是在生成小圆圈的时候之后做的。
实现:点击右侧按钮,小圆圈一同跟随变化
- 声明一个变量
circle
每次点击自增 1 ,左侧同理 - 如果
circle
为 图片的总数量,需要置为 0
//todo...
let circle = 0;
//todo...
arrow_r.addEventListener('click', function () {
//todo...
//点击右侧按钮 小圆圈跟随一起变化,可以再声明一个变量控制小圆圈的播放
circle++;
if(circle == ol.children.length){
circle = 0;
}
//排他思想
for (let i = 0; i < ol.children.length; i++){
ol.children[i].className = '';
}
ol.children[circle].className = 'current';
//todo...
})
bug小问题 :点击第三个小圆圈到第三张图片,再点击右侧按钮,居然是跳到第二张图片,为什么?
因为我们播放下一张是通过
num
来控制的 跟小圆圈点击没有任何关系,简单理解,切换到第三张图片,索引号为 2 ,修改num
的值为 2 ,需要同步,一一对应。
//todo...
let num = 0;
li.addEventListener('click', function () {
//排他思想
//点击 先清除所有li的current类名
for (let i = 0; i < ol.children.length; i++) {
ol.children[i].className = '';
}
//再将被点击的li设置current类名
this.className = 'current';
let index = this.getAttribute('index');
//当我们点击某个li 将li的索引号给num
circle = num = index;
animate(ul, -index * focusWidth);
});
//todo...
现在是到下一张图片了,但是小圆圈回到上一个,不是到下一个,我们知道
circle
是控制小圆圈的播放,将索引号也给circle
, 同步关系
左侧按钮实际上跟右侧按钮是一样的做法,逆向思考一下!
arrow_l.addEventListener('click', function () {
if (num == 0) {
// ul.style.left = -(ul.children.length - 1)* focusWidth;
// num = ul.children.length - 1;
num = ul.children.length - 1;
ul.style.left = -num * focusWidth;
}
num--;
animate(ul, -num * focusWidth);
//点击右侧按钮 小圆圈跟随一起变化,可以再声明一个变量控制小圆圈的播放
circle--;
if (circle < 0) {
circle = ol.children.length - 1;
}
//排他思想
for (let i = 0; i < ol.children.length; i++) {
ol.children[i].className = '';
}
ol.children[circle].className = 'current';
})
左侧和右侧的按钮既然排他思想的代码是一样的,不如封装到一个函数里?
function circleChange() {
//排他思想
for (let i = 0; i < ol.children.length; i++) {
ol.children[i].className = '';
}
ol.children[circle].className = 'current';
}
代码优化
if (circle < 0) {
circle = ol.children.length - 1;
}
//写成三元表达式
//todo...improve the code
circle = circle < 0 ? (ol.children.length - 1) : circle;
完成需求五:自动播放功能
- 添加定时器
setInterval
- 手动调用右侧按钮点击事件
element.click()
- 鼠标经过可视窗口,停止定时器
- 鼠标离开可视窗口,开启定时器
//todo...
var timer = setInterval(function(){
//自动播放图片 --> 点击右侧按钮
arrow_r.click();
},2000);
//todo...
focus.addEventListener('mouseenter', function () {
//todo...
clearInterval(timer);
timer = null;//清除定时器变量
});
focus.addEventListener('mouseleave', function () {
//todo...
timer = setInterval(function(){
//自动播放图片 --> 点击右侧按钮
arrow_r.click();
},2000);
});
节流阀
- 防止轮播图按钮连续点击造成播放过快
- 节流阀的目的:当上一个函数动画内容执行完毕,再执行下一个函数动画,让事件无法连续触发。
- 设置一个变量
var flag = true
if(!flag){flag = false; do something};
给函数“加锁”- 利用
animate
第三个参数传入函数(回调函数),动画函数执行完毕,设置flag = true
给函数“解锁”
//节流阀
let flag = true;
arrow_r.addEventListener('click', function () {
if(flag){
flag = false;//关闭节流阀
if (num == ul.children.length - 1) {
ul.style.left = 0;
num = 0;
}
num++;
animate(ul, -num * focusWidth);
//点击右侧按钮 小圆圈跟随一起变化,可以再声明一个变量控制小圆圈的播放
circle++;
if (circle == ol.children.length) {
circle = 0;
}
circleChange();
}
});
animate
动画函数执行完毕,就打开节流阀,可以播放下一张图片
animate(ul, -num * focusWidth, function(){
flag = true;//打开节流阀
});
左侧按钮操作同理,这里不再展示。
index.js
源码
window.addEventListener('load', function () {
//获取元素
let arrow_l = document.querySelector('.arrow-l');//左按钮
let arrow_r = document.querySelector('.arrow-r');//左按钮
let focus = document.querySelector('.focus');//可视窗口
let focusWidth = focus.offsetWidth;
let circle = 0;
let num = 0;
let flag = true;
focus.addEventListener('mouseenter', function () {
arrow_l.style.display = 'block';
arrow_r.style.display = 'block';
clearInterval(timer);
timer = null;//清除定时器变量
});
focus.addEventListener('mouseleave', function () {
arrow_l.style.display = 'none';
arrow_r.style.display = 'none';
timer = setInterval(function () {
//自动播放图片 --> 点击右侧按钮
arrow_r.click();
}, 2000);
});
let ul = focus.querySelector('ul');
let ol = focus.querySelector('.circle');
for (let i = 0; i < ul.children.length; i++) {
//创建li元素 同时将li元素插入ol
let li = document.createElement('li');
//记录当前小圆圈的索引号,通过自定义属性
li.setAttribute('index', i);
ol.appendChild(li);
li.addEventListener('click', function () {
//排他思想
//点击 先清除所有li的current类名
for (let i = 0; i < ol.children.length; i++) {
ol.children[i].className = '';
}
//再将被点击的li设置current类名
this.className = 'current';
let index = this.getAttribute('index');
//当我们点击某个li 将li的索引号给num circle
circle = num = index;
animate(ul, -index * focusWidth);
});
}
//默认第一个ol中第一个li设置为 current
ol.children[0].className = 'current';
//克隆第一张图片(li)放到ul最后
let firstNode = ul.children[0].cloneNode(true);
ul.appendChild(firstNode);
//点击右侧按钮 图片滚动一张
arrow_r.addEventListener('click', function () {
if (flag) {
flag = false;//关闭节流阀
if (num == ul.children.length - 1) {
ul.style.left = 0;
num = 0;
}
num++;
animate(ul, -num * focusWidth, function () {
flag = true;
});
//点击右侧按钮 小圆圈跟随一起变化,可以再声明一个变量控制小圆圈的播放
circle++;
if (circle == ol.children.length) {
circle = 0;
}
circleChange();
}
});
arrow_l.addEventListener('click', function () {
if (flag) {
flag = false;
if (num == 0) {
// ul.style.left = -(ul.children.length - 1)* focusWidth;
// num = ul.children.length - 1;
num = ul.children.length - 1;
ul.style.left = -num * focusWidth;
}
num--;
animate(ul, -num * focusWidth, function () {
flag = true;
});
//点击右侧按钮 小圆圈跟随一起变化,可以再声明一个变量控制小圆圈的播放
circle--;
// if (circle < 0) {
// circle = ol.children.length - 1;
// }
circle = circle < 0 ? (ol.children.length - 1) : circle;
circleChange();
}
})
function circleChange() {
//排他思想
for (let i = 0; i < ol.children.length; i++) {
ol.children[i].className = '';
}
ol.children[circle].className = 'current';
}
var timer = setInterval(function () {
//自动播放图片 --> 点击右侧按钮
arrow_r.click();
}, 2000);
})
最终效果
这个 gif
生成图我还得研究研究🧐!
第一期你应该掌握的
JavaScript
高阶技能内容就这么多啦,如果本专栏内容不错的话,望您能关注🤞点赞👍收藏❤️一键三连!