如何写好JavaScript(三大原则)| 青训营笔记

133 阅读9分钟

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

👋本笔记的基本内容:写好JS的三大原则

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

各司其责

通过深夜食堂的案例来解释“各司其责”

1.0 新手版

这段代码能够满足我们所需要的的要求,对于前段爱好者来说还是不错滴,但是如果是在多人维护的项目里呢?就有问题了。
问题: 直接修改了html和css(直接去操作了body.style),JavaScript做了css该干的的事情,违反了各司其责的原则。 单看这段代码,也许我们能猜到是完成网页深色与浅色模式的切换这样一个功能的,可是我们如果是要完成其他功能呢?每次都要去猜吗?这样敲代码的难度就大大提升了。

2.0 优化版

一般我们会通过class来定义html的状态,这个版本只是操作了html元素的状态(只是修改了classname),通过类名我们知道他们所要完成的功能,这样就解决了新手版所出现的问题了

1.0 vs 2.0 版本总结

html负责结构,css负责表现,JavaScript负责行为,结构、表现、行为分离是前端工程师所要掌握的基本原则。
如果没有遵守“各司其责"的原则,则代码会看起来很吃力,不知道代码所代表的含义,加大了代码后来的修改和维护的难度

✋那还有没有更好的版本呢?

有,因为完成网页深色与浅色模式的切换这样一个功能本质是改变html的展示效果,除了控制样式没有其他的行为逻辑在里面,一般来说,控制样式的代码可以用纯css来实现的,而根据各司其责的原则,用css来控制样式才是最合适的

3.0 无JS版

0. 切换:通过CheckBox进行切换,而用label代替button表示的🌞和🌜,通过for来绑定CheckBox实现切换功能。
1. 切换后触发伪类checked

深夜食堂--总结

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

组件封装

👋组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。
好的组件具备封装性、正确性、扩展性、复用性。
现在我们就来用轮播图来熟悉组件封装吧!

😁先从html、css、JavaScript来了解一下轮播图

1.0 无交互版本

1. 结构:HTML
轮播图是一个典型的列表结构,我们可以使用无序列表ul元素来实现。

  1. 表现:CSS
  • 使用 CSS 绝对定位将图片重叠在同一个位置
  • 轮播图切换的状态使用修饰符(modifier)
  • 轮播图的切换动画使用 CSS transition
  1. 行为:JS

轮播图使用的API

  • Slider

    • getSelectedItem( ) 得到当前选中图片元素
    • getSelectedItemIndex( ) 得到当前选中图片元素在列表的下标
    • slideTo( ) 跳转到特定的元素上
    • slideNext( ) 轮播下一张图片
    • slidePrevious( ) 轮播上一张图片

2.0 控制流交互版本

2.0 版本加上了两边控制前后翻图的箭头,下面控制选图的小圆点
行为:JS 【控制流(使用自定义事件来解耦)】
解释: 使用自定义事件是为了实现状态绑定(下面4个点与上面的图片是一一对应的) 解耦:各自独立,可灵活地替换

1.0 vs 2.0 基本方法总结

  • 结构设计

  • 展现效果

  • 行为设计

    • API (功能)
    • Event (控制流)

🤔还有改进空间呢?如果我们要重构组件的话要怎么去做咧?

原来的组件功能实现是没问题的,但组件的代码不是很灵活,比如:要删掉一两张图片,则要改动html、css、JavaScript的代码,改动不灵活

3.1 重构:插件化

解耦

  • 将控制元素抽取成插件(将两边控制前后翻图的箭头,下面控制选图的小圆点插件化 )
  • 插件与组件之间通过依赖注入方式建立联系
//将小圆点的控制抽离成一个插件pluginController
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';
      });
    }  
  }
​
//将左翻页的控制抽离成插件pluginPrevious
  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();
      });
    }  
  }
​
//将右翻页的控制抽离成插件pluginNext
  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();
      });
    }  
  }
​
//通过注册插件registerPlugins来使用各种插件
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){
    // 这里的this就是组件的实例对象
    plugins.forEach(plugin => plugin(this));
  }
}

好处:slider的代码变简单了(删除功能和新增功能都很方便)

  • 如果要删掉底下的四个小圆点,就直接删除插件,取消注册,不会影响到其他的地方,代码修改量少。
  • 如果要新增功能就再新建一个插件,注册轮播图插件。

🤔还有进一步优化的空间吗?

有,之前我们的结构是写死在html的代码里面的,但在JavaScript的UI组件里面首先要做到数据驱动,根据数据生成html的模板,不需要将结构写死在html。

3.2重构:html的模板化

Snipaste_2022-07-27_18-40-18.png

解耦

  • 将HTML模板化,(让JavaScript来渲染组件的HTML)更易于扩展
  1. render( )**渲染函数,用来渲染HTML
 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>`;
    }
  }
  1. action( ) 用来完成它的初始化
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';
      });
    }
  }
};

🤔而模板化是优化的终点吗?还能再优化吗?

3.3 重构:组件框架--抽象

Snipaste_2022-07-27_18-40-48.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 ''
    }
  }

现在就形成了一个简单的组件框架了,支持定义一个组件,然后里面注册若干个插件

总结

  • 组件设计的原则:封装性、正确性、扩展性、复用性

  • 实现组件的步骤:结构设计、展现效果、行为设计

  • 三次重构

    1. 插件化
    2. 模板化
    3. 抽象化(组件框架)

🤔组件设计会破坏“各司其责”的原则吗?

并没有破坏,因为“各司其责”的原则是html、css、JavaScript各自做他们应当做的事情,并不是看他们写在哪里。JavaScript生成的html还是负责结构,生成的css还是负责控制样式,没有违背各司其责的原理😁

当然,组件框架--抽象也不是优化的终点,如:父子组件的嵌套等,还是拥有更多的改进空间的

过程抽象

👋 过程抽象

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

前面的组件化是用来控制整体的UI组件的,过程抽象则通常是处理局部细节作用的方法。

🤔过程抽象要怎么解释呢?来看看下面这个例子

Snipaste_2022-07-28_13-44-06.png
上图是一个人在开门,我们可以把这个门,这个人抽象出来形成一个结构,门和人都是数据,而open是一个过程,也是可以抽象出来的,因为我们开门,开窗,开冰箱都可以复用open这个动作。这个过程就是过程抽象

🤔有时候我们写代码的时候会遇到有关于操作次数限制的bug,涉及一些异步交互和一次性的HTTP请求,例如:我们想让绑定的事件只在第一次点击的时候执行,之后点击都不执行。有什么解决方法呢?

常见的方法有:

  1. 设置addEventListeneronce参数 ,表示listener在添加之后最多只调用一次,如果是truelistener会在其被调用后自动移除
  2. 在回调函数中添加removeEventListener,在第一次执行后解除绑定事件
  3. 一次性执行函数Once(这就是我们要讲的过程抽象的一种情况)

高阶函数Once

为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象

 function once(fn) {
    return function(...args) {
      if(fn) {
        const ret = fn.apply(this, args);
        fn = null;
        return ret;
      }
    }
  }

我们说,如果一个函数return另一个函数,那么这个函数就叫做高阶函数。
once函数接收的参数是一个函数fn,返回了一个新的函数,在返回的函数中,做了一件事,就是让fn只执行一次,第二次执行的时候给fn已经被赋值为null,if判断为false,就无法被再次执行里面的方法了。

高阶函数 (HOF)

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

原理图:

Snipaste_2022-07-28_14-10-29.png

function HOF0(fn) {
    return function(...args) {
      return fn.apply(this, args);
    }
  }

HOF0就是一个默认的等价高阶函数,一般的高阶函数都是在HOF0的基础上去做了某些事情,比如改变某些参数/返回值等

常用的一些高阶函数(HOF)

  • Once ( 一次性执行函数
  • Throttle ( 节流函数
  • Debounce ( 防抖函数
  • Consumer / 2 ( 将同步操作变成一个异步的操作
  • Iterative ( 可迭代方法 )

编程范式

编程语言其实是有主要的编程范式,大体上可以分为命令式编程语言和声明式编程语言,像JavaScript这样的语言既有命令式编程范式的特点,又有声明式编程范式的特点,也就意味着它可以使用两种风格来写命令式的代码和声明式的代码

Snipaste_2022-07-28_14-38-46.png

🤔什么是命令式的代码,什么是声明式的代码呢?

我们有这样的一个需求,将[1, 2, 3, 4 ]这个列表里的数字乘2
命令式的方式来写代码:(命令式的话更强调怎么做

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

声明式的编程思想天然地比命令式有着更强的可扩展性

总结

  • 过程抽象 / HOF / 装饰器

过程抽象要了解高阶函数和函数装饰器

  • 命令式 / 声明式

编程语言是有命令式 / 声明式 两种风格的
JavaScript这样的语言既有命令式编程范式的特点,又有声明式编程范式的特点

Snipaste_2022-07-28_14-56-20.png