第三节 写好JavaScript | 青训营笔记

82 阅读5分钟

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

写好JS的三个原则

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

一、各司其职

在前端工程中,应该遵循HTML(结构)、表现(CSS)和行为(JavaScript)分离的原则,使得代码具有更好的可读性、扩展性和可维护性。

案例-深浅色主题切换

版本一

缺点:用js控制css


<body>
  <header>
    <button id="modeBtn">🌞</button>
    <h1>深夜食堂</h1>
  </header>
  <main>
    <div class="pic">
      <img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg">
    </div>
    <div class="description">
    </div>
  </main>
</body>

<script>
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 = '🌞';
  }
});
</script>

版本二

控制className,通过css控制样式

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

版本三

纯css实现,利用checkbox和伪类选择器:checked


<body>
  <input id="modeCheckBox" type="checkbox">
  <div class="content">
    <header>
      <label id="modeBtn" for="modeCheckBox"></label>
      <h1>深夜食堂</h1>
    </header>
  </div>
</body>


<style>
#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: '🌜';
}
</style>

总结

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

二、组件封装

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

案例-轮播图

基本方法

结构--HTML
使用无序列表ul实现图片的列表结构。

表现--css
使用className定义轮播图不同图片的状态,使用css的transition实现动画切换。

行为--JavaScript
定义轮播图的API image.png

  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';
      }
    }

总结

  • 结构设计
  • 展现效果
  • 行为设计
    • API (功能)
    • Event (控制流) 通过自定义事件解耦

重构

为了提高组件的封装性、正确性、扩展性和复用性,以上组件可以进行重构。

插件化

解耦

  • 将控制元素抽取成插件
  • 插件与组件之间通过依赖注入方式建立联系
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));
  }
...
}
function pluginController(slider){
 ... 
}
function pluginPrevious(slider){
  ... 
}

function pluginNext(slider){
  ...
}
const slider = new Slider('my-slider');
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
模板化

将HTML模板化,更易于扩展。 image.png

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="${image}"/>
      </li>    
    `.trim());
    
    return `<ul>${content.join('')}</ul>`;
  }
  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 pluginController = {
  render(images){
    return `
      <div class="slide-list__control">
        ${images.map((image, i) => `
            <span class="slide-list__control-buttons${i===0?'--selected':''}"></span>
         `).join('')}
      </div>    
    `.trim();
  },
  action(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';
      });
    }    
  }
};

...

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:3000});
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
组件抽象

将组件通用模型抽象出来 image.png

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 ''
  }
}


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);
  }
  render(data){
    const content = data.map(image => `
      <li class="slider-list__item">
        <img src="${image}"/>
      </li>    
    `.trim());
    
    return `<ul>${content.join('')}</ul>`;
  }
...
}

总结

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

三、过程抽象

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

将某一个动作(过程)抽象分离,封装为一个函数装饰器,使之应用于不同的动作并进行相应的限制。
image.png

高阶函数 HOF

Higher-Order Function,函数式编程的基础,将每一个过程抽象,可以屏蔽细节,只需要关注要实现的目标。

  • 以函数作为参数
  • 以函数作为返回值
  • 常用于作为函数装饰器

image.png

常用高阶函数

Once限制函数执行次数,只执行一次。

function once(fn) {
  return function(...args) {
    if(fn) {
      const ret = fn.apply(this, args);
      fn = null;
      return ret;
    }
  }
}
const foo = once(() => {
  console.log('bar');
});

foo();
foo();
foo();
// bar

Throttle 节流函数,限制一定时间内函数的执行次数。应用在可能发生极短时间内重复事件触发,如频繁的点击事件,提高事件的执行性能。

// 默认500ms执行一次
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 / 2 异步消耗函数,使同步事件异步执行。

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

Iterative 迭代函数,对可迭代对象调用执行函数。

const isIterable = obj => obj != null 
  && typeof obj[Symbol.iterator] === 'function';

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

使用高阶函数的好处

可以减少使用非纯函数的可能性。

  • 纯函数(pure function):确定的输入对应可预测的确定输出,不产生可观察的副作用。
  • 非纯函数:执行结果依赖于执行上下文及其状态,不可预测,可能产生副作用。

编程范式

Programming paradigm(编程范式)* 是指某种编程语言典型的编程风格或编程方式。

声明式和命令式

image.png

编程范式并不针对某一特定的语言,在 JavaScript 中,可以使用声明式的代码风格,同时也可以是命令式的。

  • 命令式
 let list = [1, 2, 3, 4];
  let mapl = [];
  for(let i = 0; i < list.length; i++) {
    mapl.push(list[i] * 2);
  }
  • 声明式
  let list = [1, 2, 3, 4];
  const double = x => x * 2;
  list.map(double);

命令式关注如何实现需求;声明式关注实现什么功能,通常具有更强的可扩展性。

image.png

总结

本次课程介绍了写好前端JS的三个原则:1.各司其职 2.组件封装 3.过程抽象。每一个原则的实现方法和代码细节设计的知识面是很宽泛的,在这里的demo仅仅是一个入门的示范,深入理解掌握其中的思想并且在实践中加以运用,有助于我们形成更优良的JavaScript代码风格。