如何写好js代码 | 青训营笔记

66 阅读5分钟

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

本次笔迹内容来源于“跟着月影学javascript”,学到了不少东西,在此记录一下。

一、写好js的一些原则

  • html,css,js各司其责:三件其职能分离
  • 组件封装:好的UI组件具备正确性、扩展性、复用性。
  • 过程抽象:应用函数式编程思想

二、用上述原则写好js的例子

本篇文章主要通过以下两个例子来展现组件封装与过程抽象的概念。

一. 写一个轮播图

一个轮播图在html上的形式为:包含n张图片以及对应的按钮,还有左右两边的跳转上一张图片和跳转下一张图片两个按钮;因此,HTML可以写成如下代码,

<div id="my-slider" class="slider-list">
  <ul>
    <li class="slider-list__item--selected">
      <img src=""/>
    </li>
    <li class="slider-list__item">
      <img src=""/>
    </li>
    <li class="slider-list__item">
      <img src=""/>
    </li>
    <li class="slider-list__item">
      <img src=""/>
    </li>
  </ul>
  <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>
</div>

然后,思考轮播图需要哪些函数 首先需要轮播的函数,以及点击跳转的函数:

slide类中包含的函数有以下五个:

 getSelectedItem(){
     //获取选中的dom元素
  }
  getSelectedItemIndex(){
      //获取选中元素的index
  }
  slideTo(idx){
      //将第idx的元素置为选中
  }
  slideNext(){
      //将下一个元素置为选中
  }
  slidePrevious(){ 
      //将前一个元素置为选中
  }

之后我们可以通过在组件类里面监听鼠标事件即可通过这些函数控制图片的选中。 代码如下所示:

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 next = this.container.querySelector('.slide-list__next');
    if(next){
      next.addEventListener('click', evt => {
        this.stop();
        this.slideNext();
        this.start();
        evt.preventDefault();
      });
    }
  }
  getSelectedItem(){
     //获取选中的dom元素
  }
  getSelectedItemIndex(){
      //获取选中元素的index
  }
  slideTo(idx){
      //将第idx的元素置为选中
  }
  slideNext(){
      //将下一个元素置为选中
  }
  slidePrevious(){ 
      //将前一个元素置为选中
  }
 }

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

然而,这种方式是否有改进的空间?

此处的构造函数过于复杂了,首先我们可以将事件控制的元素插件化,通过依赖注入联系起来。

以点击切换下一副图片为例,

  1. 首先,我们将点击事件抽象出来与slide类中的函数进行联系;
    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();
        });
      }  
    }
    
    
  2. 将插件在slide类中进行注册,即plugin函数与slide中的this进行绑定。
      registerPlugins(...plugins){
        plugins.forEach(plugin => plugin(this));
      }
    

此时,代码已经简化了不少了,但是,还可以再优化吗?

有一个问题,一开始的轮播图是限定的四个吧,要是5个或者6个呢,我们可以输入数组实现吗?

因此为了提高组件的复用性,我们可以采用模板化来进行重构,我们希望是根据数据情况来对html元素进行动态改变。

  1. 首先,我们传入的参数新增一个数组;
    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});
    
  2. 在Slide类中写一个render函数对数据构建dom;
      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>`;
      }
    
  3. 这时,事件控制的插件也需要增加自身的render函数,并在对应的dom上监听事件操作
    const pluginNext = {
      render(){
        return `<a class="slide-list__next"></a>`;
      },
      action(slider){
        const previous = slider.container.querySelector('.slide-list__next');
        if(previous){
          previous.addEventListener('click', evt => {
            slider.stop();
            slider.slideNext();
            slider.start();
            evt.preventDefault();
          });
        }  
      }
    };
    

在组件具备很高的可复用性时,我们可以将其组件化

总结:组件化用于将UI页面分割为若干组件进行组合和嵌套;组件化是一种高效的处理复杂应用系统,更好的明确功能模块作用的方式;目的是为了解耦,把复杂系统拆分成多个组件,分离组件边界和责任,便于独立升级和维护。

2.高阶函数

我所理解的过程抽象:

普通函数具备很强的确定性,比如他的功能,输入输出等,但是在实际的开发过程中,功能是确定的,比如需要在多个函数上体现这个功能等,但是输入输出不是确定的,因此,我们可以将这些确定的功能抽象出来,形成一个对普通函数或对象的约束,这就是过程抽象。

高阶函数

高阶函数是对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值输出。 常见的高阶函数有以下几个。

  1. once,只执行一次,如支付等场景:
    function once(fn) {
      return function(...args) {
        if(fn) {
          const ret = fn.apply(this, args);
          fn = null;
          return ret;
        }
      }
    }
    
    
  2. Throttle,节流函数,用户滚动浏览器滚动条的时候,就会调用后台的接口来更新页面上的某些内容。如果不对函数调用的频率加以限制的话,那么可能我们滚动一次滚动条就会产生N次的调用,损耗浏览器引擎。使用节流函数限制接口调用频率,优化性能。
    function throttle(fn,time=500){
        let timer;
        return function(...args){
            if(timer===null){
                fn.apply(this,args);
                timer = setTimeOut(()=>{
                    timer = null;
                    },time)
                 }
             }
          }
    
  3. Debounce, 防抖函数,搜索功能的实现,用户输入文本时我们会自动联想匹配出一些结果供用户选择。我们可能首先想到的做法就是监听keypress事件,然后异步去查询结果。这个方法本身是没错的,但是如果用户快速的输入了一连串的字符,假设是10个字符,那么就会在瞬间触发了10次的请求,这无疑不是我们想要的。使用防抖函数就可以实现用户停止输入的时候才去触发查询的请求。
    function debounce(fn, dur){
      dur = dur || 100;
      var timer;
      return function(){
        clearTimeout(timer);
        timer = setTimeout(() => {
          fn.apply(this, arguments);
        }, dur);
      }
    }
    
    总结防抖和节流的区别:  防抖是将多次执行变为最后一次执行,节流是将多次执行变为每隔一段时间执行
  4. 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]);
      }
    }
    

3.命令式编程与声明式编码

声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。

命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。

4.总结

多采用过程抽象,高阶函数,装饰器方式编码,多采用声明式方式进行功能抽象