javascript学习笔记|青训营笔记

114 阅读6分钟

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

⭐好的js书写原则

1. html css js 各司其职、职能分离

image.png

在一个好的项目里,html操控结构,css操控样式,js操控事件。 避免用js直接修改样式,仅使用修改类名的方式来修改样式, 一些简单的纯样式修改交互,我们可以使用纯css的方式来实现,例如下面的例子:

image.png

image.png

  • 我们为顶部的checkbox书写checked伪类选择器样式,当该checkbox被选择,切换为深夜模式,不被选择时回到正常样式。
  • 我们将右上角图标绑定为该checkbox的label,通过点击图标达到同样的效果。
  • 将checkbox使用display:none隐藏,这样就可以达到使用纯css来变化样式的效果

2. 组件封装

组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性、复用性。

简易版组件封装

以实现一个轮播图为案例,我们将该轮播图封装为一个组件用最简单的方式可以这样做:

  • 准备一份html模板
    <div id="my-slider" class="slider-list">
      <ul>
        <li class="slider-list__item--selected">
          <img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png">
        </li>
        <li class="slider-list__item">
          <img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg">
        </li>
        <li class="slider-list__item">
          <img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg">
        </li>
        <li class="slider-list__item">
          <img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg">
        </li>
      </ul>
    </div>
  • 在此基础上,我们使用css来实现轮播图表现(略),使用js来实现行为
  • 将该轮播图看为一个对象,即一个组件,我们使用构造函数的方式来生成这样一个轮播图,并将行为与模板相关联起来(传入模板el)
  • 将轮播图行为拆分为相关API,比如,向左,向右,切换到第几张图,下标控件点击等
  • 自定义一个slide事件,将图片的滚动这个状态从组件中分离出来,降低耦合性,通过监听这个滚动事件,来帮助下标圆点进行切换
class Slider{
  constructor(id, cycle = 3000){
    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('.slide-list__control');
    if(controller){
      const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
      controller.addEventListener('mouseover', evt=>{
        const idx = Array.from(buttons).indexOf(evt.target);
        if(idx >= 0){
          this.slideTo(idx);
          this.stop();
        }
      });
      
      controller.addEventListener('mouseout', evt=>{
        this.start();
      });
      
      this.container.addEventListener('slide', evt => {
        const idx = evt.detail.index
        const selected = controller.querySelector('.slide-list__control-buttons--selected');
        if(selected) selected.className = 'slide-list__control-buttons';
        buttons[idx].className = 'slide-list__control-buttons--selected';
      })
    }
    
    const previous = this.container.querySelector('.slide-list__previous');
    if(previous){
      previous.addEventListener('click', evt => {
        this.stop();
        this.slidePrevious();
        this.start();
        evt.preventDefault();
      });
    }
    
    const next = this.container.querySelector('.slide-list__next');
    if(next){
      next.addEventListener('click', evt => {
        this.stop();
        this.slideNext();
        this.start();
        evt.preventDefault();
      });
    }
  }
  getSelectedItem(){
    let selected = this.container.querySelector('.slider-list__item--selected');
    return selected
  }
  getSelectedItemIndex(){
    return Array.from(this.items).indexOf(this.getSelectedItem());
  }
  slideTo(idx){
    let selected = this.getSelectedItem();
    if(selected){ 
      selected.className = 'slider-list__item';
    }
    let item = this.items[idx];
    if(item){
      item.className = 'slider-list__item--selected';
    }
  
    const detail = {index: idx}
    const event = new CustomEvent('slide', {bubbles:true, detail})
    this.container.dispatchEvent(event)
  }
  slideNext(){
    let currentIdx = this.getSelectedItemIndex();
    let nextIdx = (currentIdx + 1) % this.items.length;
    this.slideTo(nextIdx);
  }
  slidePrevious(){
    let currentIdx = this.getSelectedItemIndex();
    let 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();

这样,我们完成了一个最简单的封装,当我们需要使用该轮播图时。只需要将html模板和js联系起来,通过构造函数,直接生成轮播图效果

image.png

然而,这样的组件,耦合性比较高,你无法将轮播图控件与轮播图分隔开。那么,我们可以将这一部分插件化。

插件化

我们将组件解耦,将控制元素抽取为插件,再通过依赖注入的方式为组件绑定插件。

class Slider{
  constructor(id, cycle = 3000){
    this.container = document.getElementById(id);
    this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
    this.cycle = cycle;
  }
  
  //仅展示新的API
  registerPlugins(...plugins){
    plugins.forEach(plugin => plugin(this));
  }
}

function pluginController(slider){
  const controller = slider.container.querySelector('.slide-list__control');
  if(controller){
    const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
    controller.addEventListener('mouseover', evt=>{
      const idx = Array.from(buttons).indexOf(evt.target);
      if(idx >= 0){
        slider.slideTo(idx);
        slider.stop();
      }
    });

    controller.addEventListener('mouseout', evt=>{
      slider.start();
    });

    slider.addEventListener('slide', evt => {
      const idx = evt.detail.index
      const selected = controller.querySelector('.slide-list__control-buttons--selected');
      if(selected) selected.className = 'slide-list__control-buttons';
      buttons[idx].className = 'slide-list__control-buttons--selected';
    });
  }  
}

function pluginPrevious(slider){
  const previous = slider.container.querySelector('.slide-list__previous');
  if(previous){
    previous.addEventListener('click', evt => {
      slider.stop();
      slider.slidePrevious();
      slider.start();
      evt.preventDefault();
    });
  }  
}

function pluginNext(slider){
  const next = slider.container.querySelector('.slide-list__next');
  if(next){
    next.addEventListener('click', evt => {
      slider.stop();
      slider.slideNext();
      slider.start();
      evt.preventDefault();
    });
  }  
}


const slider = new Slider('my-slider');
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();

现在,我们已经对使用js的所有功能进行封装,但是html还于组件为分离的状态,我们需要将html模板也进行封装

模板化

我们直接将图片传入组件,render方法生成结构并将它们渲染出来 image.png

class Slider{
  constructor(id, opts = {images:[], cycle: 3000}){
    this.container = document.getElementById(id);
    this.options = opts;
    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);
  }
  
  //仅展示新的代码
  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结构我们还是需要自己引入

抽象化

我们将整个html模板抽象化,封装进组件里,直接进行生成,在现在的html部分,我们只需要向构造函数传入一个容器,即可获得整个轮播图。(代码略)

总结

image.png

3.过程抽象

使用函数将一个操作封装起来,需要进行操作时,直接调用相关函数。 image.png 但是在过程抽象后,这个抽象的过程中可能会存在一些问题,如操作次数限制。比如说一个按键的click事件被要求只能调用一次。解决这个问题,我们有两种方法:

  • 在开启事件监听时,传入once:true的配置参数
  • 调用高级函数嵌套生成一个只能被调用一次的函数

高级函数

为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象

  function once(fn) {
    return function(...args) {
      if(fn) {
        const ret = fn.apply(this, args);
        fn = null;
        return ret;
      }
    }
  }

那么这个被剥离出来的需求作用对象是一个函数,我们称这种需求函数为高级函数(也就是对一个原始函数进行修饰,装饰器模式)

image.png

常见高级函数有:

函数名作用
once使某函数只被调用一次
throttle操作节流
debounce防抖
consumer延时表现
iterative可迭代的方法

一个超形象的解释🤣 image.png

在一个库里,推荐使用纯函数,而不是非纯函数,将一些额外的操作抽象出来。这也是推荐使用高阶函数的原因

编程泛式

分为命令式(面向过程与面向对象)与声明式。 命令式强调怎么做,而声明式只管有什么状态

image.png

新函数认识

image.png

image.png

🎉自定义事件

在这里,插入一个对我来说新的知识点,自定义事件。 在js中,除了内置的click、mouseover等事件,我们还可以自定义事件

创建事件

在js中,有两种方法创建事件

1.Event() 构造函数
let myEvent = new Event(typeArg , eventInit);

其中typeArg为自定义事件名,eventInit为事件配置对象(可选),用来控制该事件是否冒泡等,配置参数如下:

配置参数类型用处默认值
bubblesBoolean事件是否冒泡false
cancelableBoolean该事件能否被取消false
composedBoolean事件是否会在阴影根之外false
2.CustomEvent() 构造函数
let myEvent = new Event(typeArg , customEventInit);

其中typeArg为自定义事件名,customEventInit为事件配置对象(可选),配置参数如下:

配置参数类型用处默认值
detailObject配置项,传入与event相关的属性值null
bubblesBoolean事件是否冒泡false
cancelableBoolean该事件能否被取消false

事件监听

当一个对象需要监听该事件时,使用addEventListener函数开启监听

 obj.addEventListener(typeArg,cb);

事件触发

使用dispatchEvent触发相关事件

window.dispatchEvent(myEvent);