写好JS的三原则 | 青训营笔记

90 阅读4分钟

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

一、各司其职

HTML负责页面结构,CSS负责页面样式,JavaScript则负责页面动作。

实例:点击button(🌞/🌜)实现浅色模式与深色模式的切换。

image.png image.png

版本一(通过js代码直接调整样式变化)

html代码

  <header>
  <button id="modeBtn">🌞</button>
  </header>

css代码

#modeBtn {
  font-size: 2rem;
  float: right;
  border: none;
  background: transparent;
}

js代码

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

效果如下:

1.gif

版本二(实现js和css各司其职,使代码阅读更直观)

html代码同版本一

css代码

#modeBtn {
  font-size: 2rem;
  float: right;
  border: none;
  outline: none;
  cursor: pointer;
  background: inherit;
}
body.night {
  background-color: black;
  color: white;
  transition: all 1s;
}
#modeBtn::after {content: '🌞';}
body.night #modeBtn::after {content: '🌜';}

js代码

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

效果如下:

2.gif

版本三(html+css实现模式切换)

html代码

<body>
  <input id="modeCheckBox" type="checkbox">
  <div class="content">
    <header>
      <label id="modeBtn" for="modeCheckBox"></label>
    </header>
  </div>
</body>

css代码

.content {
  height: 100%;
  padding: 10px;
  transition: background-color 1s, color 1s;
}
#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: '🌜';}

效果如下:

2.gif

总结:

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

二、组件封装

组件:

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

实例:用原生JS写一个轮播图

HTML 结构设计

轮播图是一个典型的列表结构,我们可以首先使用无序列表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>
     <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 展示效果

然后,使用CSS绝对定位将图片重叠在同一个位置,轮播图切换的状态使用修饰符(modifier),轮播图的切换动画使用CSS transition。

#my-slider{
   position: relative;
   width: 790px;
}
.slider-list ul{
   list-style-type:none;
   position: relative;
   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;
}

JS 行为设计

API(功能)设计

API设计应保证原子操作,职责单一,满足灵活性。

image.png

class Slider{  //创建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');
setInterval( () => {
  slider.slideNext();
}, 2000);//每2s进行切换

效果如下:

2(1).gif

Event(控制流)

使用自定义事件来解耦。(详细代码如下)但这种方法虽然功能正常,但并不灵活,修改代码时可能“牵一发而动全身”。

重构轮播图组件

组件插件化

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

html模板化

image.png

将HTML模板化,更易于扩展(便于直接对组件的插入删除进行操作),此时的html代码可以简单写为:

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

组件抽象化

image.png

将组件通用模型抽象出来

总结:

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

三、过程抽象

用来处理局部细节控制的一些方法,函数式编程思想的基础应用。比如input(x)→ function f → output(f(x))。

认识过程抽象——实例:实现任务列表

html代码

  <ul>
    <li><button></button><span>任务一:学习HTML</span></li>
    <li><button></button><span>任务二:学习CSS</span></li>
    <li><button></button><span>任务三:学习JavaScript</span></li>
  </ul>

css代码

ul {
  padding: 0;
  margin: 0;
  list-style: none;
}
li button {
  border: 0;
  background: transparent;
  cursor: pointer;
  outline: 0 none;
}
li.completed {
  transition: opacity 2s;
  opacity: 0;
}
li button:before {content: '☑️';}
li.completed button:before {content: '✅';}

js代码

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

效果如下(可防止多次点击报错): 3.gif

常用高阶函数(HOF)

为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象,比如上面例子的js代码中对once函数的定义。 HOF的特点

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

image.png

Once

Throttle(节流函数)

Debounce(防抖函数)

Consumer

Iterative(可迭代)

编程范式

实例:实现按钮切换 5.gif

命令式

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

更易实现多种状态切换,比如三种状态的切换(修改css和js即可)

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

效果:

4.gif

四、收获总结

认识学习了JS的三大原则,使我在学习过程中可以更有条理、规范、高效的书写代码,但由于经验尚浅,仍需继续努力,深入学习!