[前端与Javascript | 青训营笔记03]

65 阅读7分钟

前言

这是我参与 [第五届青训营] 伴学笔记创作活动的第 3 天,上回讲到前端页面是由 HTMLCSSJavascript 组成的,现在就讲到Javascript

如何写好 Javascript

写好JS的一些原则

    1. 各司其职
    • HTMLCSSJavascript 职能分离
    1. 组件封装
    • 好的UI组件具备正确性、扩展性、复用性
    1. 过程抽象
    • 应用函数式编程思想

各司其职案例

案例1

写一段JS,控制一个网页,让它支持浅色和深色两种浏览模式。如果是你来实现,你会怎么做? Snipaste_2023-01-25_19-21-40.png

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

该代码是通过获取当前按钮,然后去改变body的颜色、字体颜色和图标
以上代码运用到javascript去操作css

如果你来优化,你会怎么做?

案例2

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

这一版比起上一版好在哪里?

以上代码思路是运用先定义一个标签给定CSS,然后也是获取按钮,然后改变标签,比案例1,减少了js直接操作CSS代码,将CSS样式写在标签上

还有没有其他的方案?

案例3

<body>
    <input id="modeCheckBox" type="checkbox"/>
    <div class="content">
    <header>
      <label id="modeBtn" for="modeCheckBox"></label>
      <h1>深夜食堂</h1>
    </header>
    <main>
      ...
    </main>
    </div>
</body>
#modeCheckBox {
  display:none;
}

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

以上代码就没有JS代码,就实现了各司其职的效果了

总结

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

组件封装案例

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

用原生JS 写一个电商网站的轮播图,应该怎么实现?

案例1

  • 结构: HTML 轮播图是一个典型的列表结构我们可以使用无序列表
      元素来实现。
    <div id="my-slider" class="slider-list">
      <ul>
        <li class="slider-list__item--selected"><img src="" alt=""></li>
        <li class="slider-list__item"><img src="" alt=""></li>
        <li class="slider-list__item"><img src="" alt=""></li>
        <li class="slider-list__item"><img src="" alt=""></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 ls;
      opacity: 0;
      text-align: center;
    }
    
    .slider-list_item--selected {
      transition: opacity ls;
      opacity: 1;
    }
    

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

    • Slider
      • getSelectedltem()
      • getSelectedltemIndex()
      • 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.sliderTo(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-listo  control-buttons"></span>
    </div>     
    
    const detail = {index: idx}
    const event = new CustomEvent( 'slide', {bubbles:true, detail})
    this.container.dispatchEvent(event)    
    

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

    组件封装
    总结:
    基本方法

    • 结构设计
    • 展现效果
    • 行为设计
      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));
      }
      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';
        }
    
        const detail = {index: idx}
        const event = new CustomEvent('slide', {bubbles:true, detail})
        this.container.dispatchEvent(event)
      }
      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);  
      }
      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();    
    

    此举就是将click事件进行封装,可让用户填入相应的参数就可实现功能,插件化
    里面的参数分别对应三种状态

    1. 默认状态
    2. 之前状态
    3. 下一个状态

    缺点: 当插件要进行删除时,那么也需要把模块也要去手动删除

    重构: 模板化

    解耦
    将HTML模板化,更易于扩展

    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>`;
      }
      ...
    }       
    

    除了将HTML模块化,还能怎么样进行优化呢?

    组件框架

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

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

    微信截图_20230126150432.png

    通过将公有的组件抽象出来,然后功能进行继承,达到可以复用

    总结

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

    思考: 改进空间? 这个组件是否还有进一步的改进空间?

    html模板已经可以抽象复用,状态也通过参数传递达到想要的状态,功能插件化,可进行删除和添加,大模块进行分成若干个模块,然后用函数进行封装;

    缺点:jshtml已经处理,但css并没有进行处理

    过程抽象案例

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

    微信截图_20230126153518.png

    Vue MVVC 运用了过程抽象思想
    React hooks 过程抽象 (状态管理)

    案例1

    操作次数限制

    • 一些异步交互
    • 一次性的HTTP请求
    const list = document.querySelector('ul');
    const buttons = list.querySelectorAll('button');
    buttons.forEach((button ) => {
      button.addEventListener('click',(evt) => {
        const target = evt.target;
        target.parentNode.className = 'completed' ;
      setTimeout(() => {
        list.removeChild(target.parentNode);
      },2000);
      });
    });
    

    微信截图_20230126162624.png

    如果当用户无意点击多次按钮,就会出现这个报错,除此请求数据,这个数据如果请求多次,那我们会返回第一次结果,像这种多次调用,只执行一次这种过程;我们可以运用once去过程抽象

    Once

    • 为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象
    function once(fn) {
       return function (...args) {
        if (fn) {
            const ret = fn.apply(this,args)
            fn = null
            return ret
          }
        }
    }
    
     button.addEventListener('click',once((evt) => {
        const target = evt.target;
        target.parentNode.className = 'completed' ;
      setTimeout(() => {
        list.removeChild(target.parentNode);
      },2000)
    }));    
    

    高阶函数

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

    微信截图_20230126181122.png

    function HOFO(fn) {
      return function (...args) {
       return fn.apply(this,args) 
       }
    }
    
    常用高阶函数
    • Once
    • Throttle
    • Debounce
    • Consumer/2
    • lterative

    思考和讨论

    • 为什么要使用高阶函数?

    函数的范式有种函数叫纯函数,纯函数是无状态的,传入a就得到b

    编程范式 命令式与声明式

    微信截图_20230126194511.png

    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);
    
    例子
    • Toggle- 命令式
    • Toggle- 声明式
    • Toggle- 三态 命令式
    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'
    );
    

    三态

    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 = 'warn',
      evt => evt.target.className = 'off',
      evt => evt.target.className = 'on'
    );                                
    

    声明式更具有多态,可让函数封装更灵活

    总结
    • 过程抽象/HOF/装饰器

    • 命令式/声明式

    写的js代码应该各司其职、组件封装、过程抽象.
    其中应该采用函数式编程思想,状态管理,从插件化到组件化到模块化到工程化