JavaScript | 青训营笔记

45 阅读4分钟

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

写好JS的一些原则

  • 各司其责 - 让HTML、CSS和JavaScript职能分离。
  • 组件封装 - 好的UI组件具备正确性、扩展性、复用性。
  • 过程抽象 - 应用函数式编程思想。

各司其责 - 控制一个网页,让它支持浅色和深色两种浏览模式。

版本一:

    <script>
        const btn = document.getElementById('modeBtn');
        btn.addEventListener('click', function(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 = '🌞';
        }
        }); 
    </script>

特点:在 js 代码里直接操作 css 样式,不推荐。

版本二:

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

特点:将 js 与 css 分离开,js里只是增加了类,推荐。

版本三:

        <style>
            .content{
                transition: all 1s;
            }
            #modeCheckBox {
                display: none;
            }
            #modeCheckBox:checked + .content {
                background-color: black;
                color: white;
                transition: all 1s;
            }
            #modeBtn::after{
                content: '🌞';
            }
            #modeCheckBox:checked + .content #modeBtn::after {
                content: '🌜';    
            }
        </style>
    </head>
    <body>
        <input id="modeCheckBox" type="checkbox">
        <div class="content">
            <label id="modeBtn" for="modeCheckBox" ></label>
            ...
        </div>
    </body>

特点,纯css实现,推荐。

注意:

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

组件封装 - 用原生 JS 写一个电商网站的轮播图

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

版本一(不加左右箭头和圆点选择):

结构

轮播图是一个典型的列表结构,我们可以使用无序列表ul元素来实现。

<div id="my-slider" class="slider-list">

        <ul>
          <li class="slider-list__item--selected">
            <img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png">
          </li>
        ...
        </ul>
    </div>

表现

  • 使用 CSS 绝对定位将图片重叠在同一个位置
  • 轮播图切换的状态使用修饰符(modifier)
  • 轮播图的切换动画使用 CSS transition
        #my-slider{
            position: relative;
            width: 790px;
        }
        .slider-list ul{
            list-style-type:none;
            position: relative;
        }
        .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;
        }

行为:API

Slider

  • 获取当前选中项: +getSelectedltem()
  • 获取当前选中项的索引:+getSelectedltemlndex()
  • 根据索引,跳转到指定位置图片:+slideTo()
  • 跳转到下一张图片:+slideNext()
  • 跳转到上一张图片:+slidePrevious( )
class Slider{
  constructor(id){
    this.container = document.getElementById(id);
    this.items = this.container
    .querySelectorAll('.slider-list__item, .slider-list__item--selected');
  }
  getSelectedItem(){
    const selected = this.container
      .querySelector('.slider-list__item--selected');
    return selected
  }
  getSelectedItemIndex(){
    return Array.from(this.items).indexOf(this.getSelectedItem());
  }
  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';
    }
  }
  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);  
  }
}

const slider = new Slider('my-slider');
//slider.slideTo(3);
setInterval(() => {
    slider.slideNext();
},2000)

版本二(加上左右切换和下面圆点选择):

HTML

  <a class="slide-list__next"></a>
  <a class="slide-list__previous"></a>
  <div class="slide-list__control">
    <span class="slide-list__control-buttons--selected"></span>
    <span class="slide-list__control-buttons"></span>
    ...

JS

class Slider{
  constructor(id, cycle = 3000){
    ...
    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(){
   ...
  }
  getSelectedItemIndex(){
   ...
  }
  slideTo(idx){
    ...
  }
  slideNext(){
   ...
  }
  slidePrevious(){
    ...
  }
  start(){
    this.stop();
    this._timer = setInterval(()=>this.slideNext(), this.cycle);
  }
  stop(){
    clearInterval(this._timer);
  }
}

const slider = new Slider('my-slider');
slider.start();

版本四:将 js 代码插件化

解耦

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

pluginController, pluginPrevious, pluginNext 抽离出来

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;
  }
  registerPlugins(...plugins){
    plugins.forEach(plugin => plugin(this));
  }
  getSelectedItem(){
    ...
  }
  getSelectedItemIndex(){
    ...
  }
  slideTo(idx){
   ...
    }
    const 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(){
    ...
  }
  slidePrevious(){
   ...
  }
  addEventListener(type, handler){
    this.container.addEventListener(type, handler)
  }
  start(){
    ...
  }
  stop(){
    ...
  }
}

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 slider = new Slider('my-slider');
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();

版本五:模板化

将HTML模板化,更易于扩展

1.png

HTML

<div id="my-slider" class="slider-list"></div>

版本六:组件框架

抽象

  • 将组件通用模型抽象出来

2.png

  • Slider:组件(可继承Component通用组件)

  • Component:通用组件(两个抽象方法)
    registerPlugins:注册插件
    render:渲染

  • SliderPlugin:渲染

总结

  • 组件设计的原则:封装性、正确性、扩展性、复用性
  • 实现组件的步骤:结构设计、展现效果、行为设计
  • 三次重构
    1. 插件化
    2. 模板化
    3. 抽象化(组件框架)

过程抽象

  • 用来处理局部细节控制的一些方法
  • 函数式编程思想的基础应用

操作次数限制

  • 一些异步交互
  • 一次性的HTTP请求

once 函数,只调用一次

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

高阶函数

HOF

  • 以函数作为参数
  • 以函数作为返回值
  • 常用于作为函数装饰器
  function HOF0(fn) {
    return function(...args) {
      return fn.apply(this, args);
    }
  }

once 函数(执行一次)

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

Throttle 函数(节流函数,固定时间间隔执行)

function throttle(fn, time = 500){
  let timer;
  return function(...args){
    if(timer == null){
      fn.apply(this,  args);
      timer = setTimeout(() => {
        timer = null;
      }, time)
    }
  }
}

Debounce 函数(防抖函数)

function debounce(fn, dur){
  dur = dur || 100;
  var timer;
  return function(){
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, dur);
  }
}

Consumer 函数

function consumer(fn, time){
  let tasks = [],
      timer;
  
  return function(...args){
    tasks.push(fn.bind(this, ...args));
    if(timer == null){
      timer = setInterval(() => {
        tasks.shift().call(this)
        if(tasks.length <= 0){
          clearInterval(timer);
          timer = null;
        }
      }, time)
    }
  }
}

Consumer 函数1 (间隔时间调用或延时调用)

function consumer(fn, time){
  let tasks = [],
      timer;
  
  return function(...args){
    tasks.push(fn.bind(this, ...args));
    if(timer == null){
      timer = setInterval(() => {
        tasks.shift().call(this)
        if(tasks.length <= 0){
          clearInterval(timer);
          timer = null;
        }
      }, time)
    }
  }
}

btn.onclick = consumer((evt)=>{
  let t = parseInt(count.innerHTML.slice(1)) + 1;
  count.innerHTML = `+${t}`;
  count.className = 'hit';
  let r = t * 7 % 256,
      g = t * 17 % 128,
      b = t * 31 % 128;
  
  count.style.color = `rgb(${r},${g},${b})`.trim();
  setTimeout(()=>{
    count.className = 'hide';
  }, 500);
}, 800)

Iterative 函数 (迭代函数,批量操作)

function iterative(fn) {
  return function(subject, ...rest) {
    if(isIterable(subject)) {
      const ret = [];
      for(let obj of subject) {
        ret.push(fn.apply(this, [obj, ...rest]));
      }
      return ret;
    }
    return fn.apply(this, [subject, ...rest]);
  }
}

编程范式

命令式(过程)

switcher.onclick = function(evt){
  if(evt.target.className === 'on'){
    evt.target.className = 'off';
  }else{
    evt.target.className = 'on';
  }
}

声明式(结果)

function toggle(...actions){
  return function(...args){
    let action = actions.shift();
    actions.push(action);
    return action.apply(this, args);
  }
}

switcher.onclick = toggle(
  evt => evt.target.className = 'off',
  evt => evt.target.className = 'on'
);