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

153 阅读5分钟

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

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

三原则

  • 各司其职

让HTML, CSS, JavaScript职能分离,不侵入对方的领域

  • 组件封装

好的UI组件应当具备正确性、扩展性、复用性

  • 过程抽象

应用函数式编程思想,抽象出一个统一的接口,让外部调用者可以解耦

各司其职

案例

深夜食堂

需求: 有一个网页,让它支持浅色和深色两种浏览模式,通过点击按钮来切换模式。如果是你来实现,你会怎么做?

案例一

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

存在问题:

  • 通过JS操作了DOM数据,违背了原则:各司其职
  • 后续维护麻烦,效果不直观

优化方向:

  • 可读性

版本二

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

优化之处:

  • 通过ClassName修改元素状态
  • 通过night表明这是一个夜间效果切换

继续优化方向:

  • 使用全CSS来控制样式
<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>
        这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈
        眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返 [6]  。
    </p>
    </div>
</main>
</div>

<style>
 #modeCheckBox {
    display: none;
}

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

优点:

  • 通过CSS控制样式

结论

  • HTML/CSS/JS各司其职
  • 应该避免不必要的由JS直接操作样式
  • 可以用class来表示状态
  • 纯展示类交互寻求零 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>
        <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>

表现

#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{
    /* 使用 CSS 绝对定位将图片重叠在同一个位置 */
    position: absolute;
    /* 切换效果 */
    transition: opacity 1s;
    /* 不透明度 即当前图片为透明 */
    opacity: 0;
    text-align: center;
}

/* 设置其显示 */
.slider-list__item--selected{
    transition: opacity 1s;
    opacity: 1;
}

行为

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

控制

<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>
  <span class="slide-list__control-buttons"></span>
  <span class="slide-list__control-buttons"></span>
</div>
<script>
  const detail = {index: idx}
  const event = new CustomEvent('slide', {bubbles:true, detail})
  this.container.dispatchEvent(event)
</script>

基本方法

分开三部分设计,尽量互不干扰职责

  • 结构
  • 展现
  • 行为
    1. API (功能)
    2. Event (控制流)

优化

改进空间:

控制元素不够灵活,一处改变需要改对应的多个位置

插件化

解耦

  • 将控制元素抽取成插件
  • 插件和组件之间通过依赖注入的方式建立联系
class Slider{
  // ...略去重复代码
  registerPlugins(...plugins){
    plugins.forEach(plugin => plugin(this));
  }
  addEventListener(type, handler){
    this.container.addEventListener(type, handler)
  }
  start(){
    this.stop();
    this._timer = setInterval(()=>this.slideNext(), this.cycle);
  }
  stop(){
    clearInterval(this._timer);
  }
}

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

模板化

根据模板生成元素

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>`;
  }
  // 略去重复代码
}

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

抽象

将多个组件的通用模型抽象出来 抽象出一个统一的接口

// 父类
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{
    // 具体实现及子类方法
}

总结

组件设计原则

  • 封装性
  • 正确性
  • 扩展性
  • 复用性

实现组件的步骤

结构设计 -> 展现效果 -> 行为设计

过程抽象

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

过程抽象

高阶函数

HOF(Higher-order function)

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

然后再调用生成的函数

使用场景:当需要对函数进行装饰时,可以使用高阶函数,例如限流器、防抖器等,提高可读性,减少非纯函数

案例

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

btn.onclick = throttle(function(e){
  circle.innerHTML = parseInt(circle.innerHTML) + 1;
  circle.className = 'fade';
  setTimeout(() => circle.className = '', 250);
});

编程范式

命令式

更强调怎么做

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

案例

命令式

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