跟着月影学 JavaScript | 青训营笔记

58 阅读4分钟

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

本次课程从实践维度解读在实际编码过程中何种类型的 JavaScript 代码称之为“好代码”,并从 JS 出发,总结其他语言编码可遵循的共性原则。

写好JS的三大原则

  • 各司其职:让HTML、CSS和JavaScript三者分离(指自己处理自己应该处理的事)
  • 组件封装:组件应当具有准确性、可拓展性和复用性
  • 过程抽象:函数式编程思想

各司其职(深夜食堂)

const btn = document.getElementById("modeBtn");
btn.addEventListener("click", (e) => {
  const body = document.body;
  if (e.target.innerHTML === "🌞") {
    body.style.backgroundColor = "black";
    body.style.color = "white";
    e.target.innerHTML = "🌜";
  } else {
    body.style.backgroundColor = "white";
    body.style.color = "black";
    e.target.innerHTML = "🌞";
  }
});


const btn = document.getElementById("modeBtn");
btn.addEventListener("click", (e) => {
  const body = document.body;
  if (body.className !== "night") {
    body.className = "night";
  } else {
    body.className = "";
  }
});

上述两种js代码可能看起来不是很复杂,但是却违背了js三大原则之一的各司其职原则,通过js来直接更改css中的属性值或类名。

改进

//html
<input id="modeCheckBox" type="checkbox">
<div class="content">
    <header>
        <label id="modeBtn" for="modeCheckBox"></label>
        <h1>深夜食堂</h1>
    </header>
    <main>
        <div class="pic">
            <img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg">
        </div>
        <div class="description">
            <p>
            这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈眶。
            </p>
        </div>
    </main>
</div>
//css
#modeCheckBox {
  display: none;
}

#modeCheckBox:checked + .content {
  background-color: black;
  color: white;
  transition: all 1s;
}

#modeBtn {
  font-size: 2rem;
  float: right;
}

#modeBtn::after {
  content: '🌞';
}

#modeCheckBox:checked + .content #modeBtn::after {
  content: '🌜';
}

使用css中的伪类选择器来实现样式的改变,做到了用css来更改css样式。

  • HTML/CSS/JS各司其职
  • 应当避免不必要的由JS直接操作样式
  • 可以用class来表示状态
  • 纯展示类交互寻求零JS方案

组件封装

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();

上述代码能完成轮播图效果的制作,但存在着不足,上述代码中,控制元素和组件是绑定在一块的,当用户不想要某个控制元素,如下面的控点,需要修改很多代码。

通过将控制元素抽取成插件,插件与组件之间通过依赖注入的方式来建立联系

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();
    });
  }  
}

这样子,当我们想要新增一个插件时,就不需要更改很多代码,只需将组件注册到代码中即可,但当我们要删除部分组件时,还需到html中删除部分标签。

通过将HTML模板化,方便进行扩展

class Slider{
    constructor(id, opts = { images: [], cycle: 3000 }) {
        ...
    }
    render() {
        const images = this.options.images;
        const content = images.map(image => `
            <li class="slider-list__item">
                <img src="${iamge}" />
            </li>
            `.trim());
        
        return `<ul>${content.join('')}</ul>`;
    }
    ...
}

将通用的组件模型抽象出来,用来优化代码。

class Component{
    constructor(id, opts = {name, data:[]}){
        this.container = document.getElementById(id);
        this.options = opts;
        this.container.innerHTML = this.render(opts.data);
    }
    registerPlugins(...plugins){
        plugins.forEach(plugin => {
            const pluginContainer = document.createElement('div');
            pluginContainer.className = `.${name}__plugin`;
            pluginContainer.innerHTML = plugin.render(this.options.data);
            this.container.appendChild(pluginContainer);

            plugin.action(this);
        });
    }
    render(data) {
        /* abstract */
        return ''
    }
}

过程抽象

用来处理局部细节控制

image.png

在想象过程抽象中,可以抽象成有一个房间,房间里面的门,窗,然后房间空间本身都是数据,但是开门或者开窗的开这个动作就是过程,也就说我们不仅可以将门,窗,空间抽象成数据,开这个过程也是可以来作为抽象对象的。

操作次数限制

通过高阶函数,以函数作为参数,就能得到写出一个只执行一次或者限制次数的函数,例如下面Once()函数,这个函数的参数是一个函数fn,然后执行时会返回一个函数出去,在返回函数中间进行判断fn是否存在,如果存在则用实际参数执行fn,然后将fn=null这样下次就不会执行该函数。

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

总结

对于JS来讲,我以前学的只是一些皮毛,通过课程,我了解了编码的三大原则,也对JS有了一个更深入的了解。与此同时,我也看到了自己的不足,需要更加深入地去学习一下JS。