这是我参与「第四届青训营 」笔记创作活动的的第3天
该文章是根据笔者在青训营课程与MDN上的js介绍以及自己所学编写而成,多为复习而用,人菜勿喷(求饶buff叠满)
青训营的js课程是月影佬来讲述,佬的思想把js提高到一个让我难以理解的高度(人话就是孩子蠢,听的不太懂QAQ),这个笔记就根据课程的轮播图一步步封装实现来分析代码,希望能让你我都明白。
写好js的原则
这点还是非常清晰的,各种语言做各自的事情才能效果更好,你总不能让服务员去负责部分大厨的工作,让大厨去负责部分收银员的工作吧(笑)。
结构--HTML
首先要写出大致布局的html,用无序列表放入图片,加入左右切换的小箭头和下方控制图片转换的小圆点
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>轮播图实现</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="my-slider" class="slider-list">
<ul>
<li class="slider-list__item--selected">
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2a18f987c51a49caab8980c1d38baeaf~tplv-k3u1fbpfcp-zoom-1.image">
</li>
<li class="slider-list__item">
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/74b4abac2966497e81c231813a236360~tplv-k3u1fbpfcp-zoom-1.image">
</li>
<li class="slider-list__item">
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cdba06579a13475e8a554ba9abc26138~tplv-k3u1fbpfcp-zoom-1.image ">
</li>
<li class="slider-list__item">
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2a61aee3d95c4b3d88807a11e07ec7a2~tplv-k3u1fbpfcp-zoom-1.image">
</li>
</ul>
<a class="slider-list__next"></a>
<a class="slider-list__previous"></a>
<div class="slider-list__control">
<span class="slider-list__control-buttons--selected"></span>
<span class="slider-list__control-buttons"></span>
<span class="slider-list__control-buttons"></span>
<span class="slider-list__control-buttons"></span>
</div>
</div>
<script src="./index.js"></script>
</body>
</html>
样式CSS
接下来是用CSS渲染html,让轮播图富有美感同时更接近我们想要的样子(注释什么的就大概标在代码中了,毕竟重点不是在css这块)
#my-slider{
position: relative;
width: 790px;
height: 340px;
}
.slider-list ul{
list-style-type:none;
position: relative;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
.slider-list__item,
.slider-list__item--selected{
/*子绝父相(子元素绝对定位,父元素相对定位),如果不知道的话可以百度一下*/
position: absolute;
/*透明度变化动画*/
transition: opacity 1s;
opacity: 0;
text-align: center;
}
.slider-list__item--selected{
/*透明度变化动画*/
transition: opacity 1s;
opacity: 1;
}
/* 前后控制元素样式 */
.slider-list__next,
.slider-list__previous{
display: inline-block;
position: absolute;
top: 50%;
width: 30px;
height: 50px;
margin-top: -25px;
/* -------------- */
text-align: center;
font-size: 24px;
line-height: 50px;
overflow: hidden;
border: none;
background: transparent;
color: white;
background: rgba(0, 0, 0, 0.2);
/*将鼠标移入效果变成手指状*/
cursor: pointer;
opacity: 0;
/*透明度变化动画*/
transition: opacity 0.5s;
}
.slider-list__next {
right: 0;
}
.slider-list__previous {
left: 0;
}
#my-slider:hover .slider-list__next {
opacity: 1;
}
#my-slider:hover .slider-list__previous {
opacity: 1;
}
.slider-list__next::after {
/* 显示样式 下方同理*/
content: '>';
}
.slider-list__previous::after {
content: '<';
}
/* 下方控制的圆点 */
.slider-list__control {
position: relative;
display: table;
background-color: rgba(255, 255, 255, 0.5);
padding: 5px;
border-radius: 12px;
bottom: 30px;
margin: auto;
}
.slider-list__control-buttons,
.slider-list__control-buttons--selected{
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;
margin: 0 5px;
background-color: white;
/*将鼠标移入效果变成手指状*/
cursor: pointer;
}
.slider-list__control-buttons--selected {
/*当选择后,小圆点的颜色变成红色*/
background-color: red;
}
行为JS
class Slider{
constructor(id, cycle = 3000){
// 通过id获取容器
this.container = document.getElementById(id);
// 获取所有的列表项
this.items = this.container
.querySelectorAll('.slider-list__item, .slider-list__item--selected');
// 获取定时器时间
this.cycle = cycle;
// 获取圆点控制器
const controller = this.container.querySelector('.slider-list__control');
if (controller){
const buttons = controller.querySelectorAll('.slider-list__control-buttons, .slider-list__control-buttons--selected');
// 给圆点绑定事件--当鼠标放上去,将对应圆点图片显示出来,且此时循环停止
controller.addEventListener('mouseover', e => {
const idx = Array.from(buttons).indexOf(e.target);
if(idx >= 0) {
this.slideTo(idx);
this.stop();
};
});
// 给圆点绑定离开恢复循环
controller.addEventListener('mouseout', e => {
this.start();
});
// 注册一个slide事件,将选中的图片与小圆点设置为selected状态
this.container.addEventListener('slide', e => {
const idx = e.detail.index;
const selected = controller.querySelector('.slider-list__control-buttons--selected');
if(selected) {
selected.className = 'slider-list__control-buttons';
buttons[idx].className = 'slider-list__control-buttons--selected';
}
})
}
// 给左侧箭头绑定翻到前一页
const previous = this.container.querySelector('.slider-list__previous');
if(previous) {
previous.addEventListener('click', e => {
this.stop();
this.slidePrevious();
this.start();
e.preventDefault();
})
}
// 给右侧箭头绑定翻到后一页
const next = this.container.querySelector('.slider-list__next');
if (next) {
next.addEventListener('click', e => {
this.stop();
this.slideNext();
this.start();
e.preventDefault();
})
}
}
// 获取当前选择的列表项
getSelectedItem(){
const selected = this.container
.querySelector('.slider-list__item--selected');
return selected
}
// 获取当前选择项的索引
getSelectedItemIndex(){
return Array.from(this.items).indexOf(this.getSelectedItem());
}
// 切换至,思路就是先将当前选择的取消,再通过索引,将目标项的class修改为选择
slideTo(idx){
const selected = this.getSelectedItem();
if(selected){
selected.className = 'slider-list__item';
}
const item = this.items[idx];
if(item){
item.className = 'slider-list__item--selected';
}
const detail = {index: idx};
// 创建'slide'事件,CustomEvent中"detail"可选的默认值是 null 的任意类型数据,是一个与 event 相关的值,bubbles 一个布尔值,表示该事件能否冒泡,cancelable 一个布尔值,表示该事件是否可以取消。
const event = new CustomEvent('slide', {bubbles: true, detail});
// 向container中派发事件
this.container.dispatchEvent(event);
}
// 下一个
slideNext(){
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
// 上一个
slidePrevious(){
const currentIdx = this.getSelectedItemIndex();
const previousIdx = (this.items.length + currentIdx - 1)
% this.items.length;
this.slideTo(previousIdx);
}
// 循环开始
start(){
// 先清空上一个计时器
this.stop();
this._timer = setInterval(() => this.slideNext(), this.cycle);
}
// 循环停止,即停止计时器运转
stop(){
clearInterval(this._timer);
}
}
const slider = new Slider('my-slider');
slider.start();
组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性、复用性。
而我们这样的js代码做到了这样的要求吗?显然没有。如果增加一个功能,代码可以复用吗?如果修改一个功能,代码只需要略微变动还是大刀阔斧的修改?显然,这样的代码进步修改空间还很大。
重构:插件化
解耦
- 将控制元素抽取成插件
- 插件与组件之间通过依赖注入方式建立联系
之前的代码已经能找出js控制的三个元素,分别为底部圆点控制器,左侧翻页键和右侧翻页键,接下来我们将其拆解为三个组件
首先是底部圆点控制器组件pluginController
// 圆点控制件
function pluginController(slider) {
// 获取圆点控制器
const controller = slider.container.querySelector('.slider-list__control');
if (controller){
const buttons = controller.querySelectorAll('.slider-list__control-buttons, .slider-list__control-buttons--selected');
// 给圆点绑定事件--当鼠标放上去,将对应圆点图片显示出来,且此时循环停止
controller.addEventListener('mouseover', e => {
const idx = Array.from(buttons).indexOf(e.target);
if(idx >= 0) {
slider.slideTo(idx);
slider.stop();
};
});
// 给圆点绑定离开恢复循环
controller.addEventListener('mouseout', e => {
slider.start();
});
// 注册一个slide事件,将选中的图片与小圆点设置为selected状态
slider.container.addEventListener('slide', e => {
const idx = e.detail.index;
const selected = controller.querySelector('.slider-list__control-buttons--selected');
if(selected) {
selected.className = 'slider-list__control-buttons';
buttons[idx].className = 'slider-list__control-buttons--selected';
}
});
}
}
其次是左侧翻页键
// 向左翻页插件
function pluginPrevious(slider) {
// 给左侧箭头绑定翻到前一页
const previous = slider.container.querySelector('.slider-list__previous');
if(previous) {
previous.addEventListener('click', e => {
slider.stop();
slider.slidePrevious();
slider.start();
e.preventDefault();
});
}
}
最后是右侧翻页键
// 向右翻页插件
function pluginNext(slider) {
// 给右侧箭头绑定翻到后一页
const next = slider.container.querySelector('.slider-list__next');
if (next) {
next.addEventListener('click', e => {
slider.stop();
slider.slideNext();
slider.start();
e.preventDefault();
})
}
}
不难看出,组合的这些组件都是从之前的js代码中组合出来的
接下来我们将其放到Slider类中使用一个registerPlugins使用这些组件
class Slider{
constructor(id, cycle = 3000){
// 通过id获取容器
this.container = document.getElementById(id);
// 获取所有的列表项
this.items = this.container
.querySelectorAll('.slider-list__item, .slider-list__item--selected');
// 获取定时器时间
this.cycle = cycle;
}
// 注册组件
registerPlugins(...plugins) {
plugins.forEach(plugin => plugin(this));
}
// ...下方省略原本写的API代码
}
这种将依赖对象传入插件初始化函数的方式,叫做依赖注入,是一种组件与插件解耦的基本思路。
const slider = new Slider('my-slider');
// 注册插件
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
重构:模板化
解耦
- 将HTML模板化,更易于扩展
在使用时我们无法确定插入的图片数量,所以应该做到组件能够定制不同数量照片均能实现功能。
class Slider{
constructor(id, opts = {
images: [],
cycle: 3000
}){
// 通过id获取容器
this.container = document.getElementById(id);
// 获取配置
this.options = opts;
// 渲染HTML
this.container.innerHTML = this.render();
// 获取所有的列表项
this.items = this.container
.querySelectorAll('.slider-list__item, .slider-list__item--selected');
// 获取定时器时间
this.cycle = opts.cycle || 3000;
this.slideTo(0);
}
// 渲染HTML用的render函数
// trim()的作用就是删除两端的空白符的
render() {
const images = this.options.images;
const content = images.map(image => `
<li class="slider-list__item">
<img src="${image}" />
</li>
`.trim());
return `<ul>${content.join('')}</ul>`;
}
}
接下来是渲染HTML的render和用来注册自定义事件的action
// 圆点控制件
const pluginController = {
render(images) {
return `
<div class="slider-list__control">
${images.map((image, i) => `
<span class="slider-list__control-buttons${i === 0 ? '--selected' : ''}"></span>
`).join('')}
</div>
`.trim();
},
// 行为
action(slider) {
// 获取圆点控制器
const controller = slider.container.querySelector('.slider-list__control');
if (controller){
const buttons = controller.querySelectorAll('.slider-list__control-buttons, .slider-list__control-buttons--selected');
// 给圆点绑定事件--当鼠标放上去,将对应圆点图片显示出来,且此时循环停止
controller.addEventListener('mouseover', e => {
const idx = Array.from(buttons).indexOf(e.target);
if(idx >= 0) {
slider.slideTo(idx);
slider.stop();
};
});
// 给圆点绑定离开恢复循环
controller.addEventListener('mouseout', e => {
slider.start();
});
// 注册一个slide事件,将选中的图片与小圆点设置为selected状态
slider.container.addEventListener('slide', e => {
const idx = e.detail.index;
const selected = controller.querySelector('.slider-list__control-buttons--selected');
if(selected) {
selected.className = 'slider-list__control-buttons';
buttons[idx].className = 'slider-list__control-buttons--selected';
}
});
}
}
}
// 向左翻页插件
const pluginPrevious = {
render() {
return `<a class="slider-list__previous"></a>`;
},
action(slider) {
// 给左侧箭头绑定翻到前一页
const previous = slider.container.querySelector('.slider-list__previous');
if(previous) {
previous.addEventListener('click', e => {
slider.stop();
slider.slidePrevious();
slider.start();
e.preventDefault();
});
}
}
}
// 向右翻页插件
const pluginNext = {
render() {
return `<a class="slider-list__next"></a>`
},
action(slider) {
// 给右侧箭头绑定翻到后一页
const next = slider.container.querySelector('.slider-list__next');
if (next) {
next.addEventListener('click', e => {
slider.stop();
slider.slideNext();
slider.start();
e.preventDefault();
})
}
}
}
接下来注册插件
// 注册组件
registerPlugins(...plugins) {
plugins.forEach(plugin => {
const pluginContainer = document.createElement('div');
pluginContainer.className = '.slider-list__plugin';
pluginContainer.innerHTML = plugin.render(this.options.images);
this.container.appendChild(pluginContainer);
plugin.action(this);
});
}
这样便可以使用组件了。
const slider = new Slider('my-slider', {
images: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png',
'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg',
'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg',
'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'
],
cycle: 1000
});
slider.registerPlugins(pluginPrevious, pluginNext);
slider.start();
模板化搞定,已经可以控制不同数量图片的使用了
抽象
不多说,直接上代码
class Component {
constructor(id, opts = {name, data:[]}) {
// 通过id获取容器
this.container = document.getElementById(id);
// 获取配置
this.options = opts;
// 渲染HTML
this.container.innerHTML = this.render(opts.data);
}
// 注册组件
registerPlugins(...plugins) {
plugins.forEach(plugin => {
const pluginContainer = document.createElement('div');
pluginContainer.className = `.${this.options.name}__plugin`;
pluginContainer.innerHTML = plugin.render(this.options.data);
this.container.appendChild(pluginContainer);
plugin.action(this);
});
}
render(data) {
/* abstract */
return ''
}
}
class Slider extends Component{
constructor(id, opts = {
name: 'slider-list',
data: [],
cycle: 3000
}){
super(id, opts);
// 获取所有的列表项
this.items = this.container
.querySelectorAll('.slider-list__item, .slider-list__item--selected');
// 获取定时器时间
this.cycle = opts.cycle || 3000;
this.slideTo(0);
}
// 渲染HTML用的render函数
// trim()的作用就是删除两端的空白符的
render(data) {
const content = data.map(image => `
<li class="slider-list__item">
<img src="${image}" />
</li>
`.trim());
return `<ul>${content.join('')}</ul>`;
}
// 获取当前选择的列表项
getSelectedItem(){
const selected = this.container
.querySelector('.slider-list__item--selected');
return selected
}
// 获取当前选择项的索引
getSelectedItemIndex(){
return Array.from(this.items).indexOf(this.getSelectedItem());
}
// 切换至,思路就是先将当前选择的取消,再通过索引,将目标项的class修改为选择
slideTo(idx){
const selected = this.getSelectedItem();
if(selected){
selected.className = 'slider-list__item';
}
const item = this.items[idx];
if(item){
item.className = 'slider-list__item--selected';
}
const detail = {index: idx};
// 创建'slide'事件
const event = new CustomEvent('slide', {bubbles: true, detail});
// 向container中派发事件
this.container.dispatchEvent(event);
}
// 下一个
slideNext(){
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
// 上一个
slidePrevious(){
const currentIdx = this.getSelectedItemIndex();
const previousIdx = (this.items.length + currentIdx - 1)
% this.items.length;
this.slideTo(previousIdx);
}
// 循环开始
start(){
this.stop();
this._timer = setInterval(() => this.slideNext(), this.cycle);
}
// 循环停止
stop(){
clearInterval(this._timer);
}
}
const slider = new Slider('my-slider', {
name: 'slider-list',
data: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png',
'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg',
'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg',
'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'
],
cycle: 1000
});
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
高阶函数
操作次数限制
- 一些异步交互
- 一次性的HTTP请求 这段代码在每次点击时延时 2s 后移除该节点
const list = document.querySelector('ul');
const buttons = list.querySelectorAll('button');
buttons.forEach((button) => {
button.addEventListener('click', (evt) => {
const target = evt.target;
target.parentNode.className = 'completed';
setTimeout(() => {
list.removeChild(target.parentNode);
}, 2000);
});
});
然后就,光荣的报错了,既然报错了,那就别学了(bushi)
这个报错原因是为什么呢?我们仔细的看一下代码,既然是2s之后移除节点,那么在移除之前我们重复多次点呢?
function once(fn) {
return function(...args) {
if(fn) {
const ret = fn.apply(this, args);
fn = null;
return ret;
}
}
}
const list = document.querySelector('ul');
const buttons = list.querySelectorAll('button');
buttons.forEach((button)=>{
button.addEventListener('click', once((evt) => {
const target = evt.target;
target.parentNode.className = 'completed';
setTimeout(()=>{
list.removeChild(target.parentNode);
},2000);
}))
});
函数once接收一个函数,判断是否为null,如果已经执行便将fn赋值为null,为null时无法执行,函数将无法进行任何操作,这样便做到了只能执行一次的函数。
ps:例子很棒,但我的表述可能并不好(悲)常用高阶函数
HOF
- Once
- Throttle
- Debounce
- Consumer / 2
- Iterative 这些分析我觉得cos佬写的特别好(像佬学习) 原文地址在此:ysx.cosine.ren/note/front-…
总结
月影老师讲解的javascript对我来说仿佛打开了js的新大门,越学越应该虚心。