Javascript介绍 | 青训营笔记

123 阅读8分钟

这是我参与「第四届青训营 」笔记创作活动的的第3天

该文章是根据笔者在青训营课程与MDN上的js介绍以及自己所学编写而成,多为复习而用,人菜勿喷(求饶buff叠满)

青训营的js课程是月影佬来讲述,佬的思想把js提高到一个让我难以理解的高度(人话就是孩子蠢,听的不太懂QAQ),这个笔记就根据课程的轮播图一步步封装实现来分析代码,希望能让你我都明白。

写好js的原则

微信截图_20220729191513.png

这点还是非常清晰的,各种语言做各自的事情才能效果更好,你总不能让服务员去负责部分大厨的工作,让大厨去负责部分收银员的工作吧(笑)。

结构--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模板化,更易于扩展

t0196498fb325ccb123.png

在使用时我们无法确定插入的图片数量,所以应该做到组件能够定制不同数量照片均能实现功能。

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)

QQ截图20220806235444.png 这个报错原因是为什么呢?我们仔细的看一下代码,既然是2s之后移除节点,那么在移除之前我们重复多次点呢?

这么快就找到bug原因了,不愧是我(叉腰)

那么接下来就要引入一个新的概念了——高阶函数

在这个例子中,我们要保证在这个期间函数只能执行一次,为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象

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的新大门,越学越应该虚心。