如何学好JavaScript(一) | 青训营笔记

87 阅读3分钟

写好JavaScript的原则

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

1. 各司其职

  1. 结构、表现、行为分离

  2. 三种方案改进:

    a. JS直接操作DOM修改样式

    b. JS只更改CSS类名

    c. 纯CSS实现(结合checkbox的伪类checked + label的for)

2. 组件封装

对于一个好的组件而言,具有封装性正确性扩展性复用性

结构设计 👉 展示效果 👉 行为设计(Api设计、Event控制流)

  • 依赖注入 + 插件封装

  • HTML的模板化 => 更利于扩展

  • 抽象成模型

HTML代码如下:

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

(1)原始想法: 通过API的方式,封装Slide类的行为

Slide

  • getSelectedItem () // 获取当前的元素
  • getSelectedIndex () // 获取当前选中的索引值 - 内部调用getSelectedItem(), 找到元素
  • slideTo (index) // 滑动到指定索引 - 内部调用getSelectedIndex(), 清除状态
  • slideNext () // 滑动到下一个 - 内部调用SlideTo()
  • slidePrev () // 滑动到上一个 - 内部调用SlideTo()
class Slide {
  constructor (id) {
    this.container = document.getElementById(id)
    // 不一定要通过document进行querySelector
    this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected')
    // 记录当前的元素长度
    this.len = this.items.length

    const prev = this.container.querySelector('.slide-list__previous')
    const next = this.container.querySelector('.slide-list__next')
    
    if (prev) {
      prev.addEventListener ('click', event => {
        this.slidePrev()
        event.preventDefault()
      })
    }

    if (next) {
      next.addEventListener ('click', event => {
        this.slideNext()
        event.preventDefault()
      })
    }
  }

  

  getSelectedItem () {
    return this.container.querySelector('.slider-list__item--selected')
  }

  getSelectedIndex () {
    return Array.from(this.items).indexOf(this.getSelectedItem())
  }

  slideTo (index) {
    this.items[this.getSelectedIndex()].className = 'slider-list__item'

    const target = this.items[index]
    target.className = 'slider-list__item--selected'
  }

  slideNext () {
    let index = this.getSelectedIndex()
    this.slideTo(++index % this.len)
  }

  slidePrev () {
    let index = this.getSelectedIndex()
    this.slideTo((this.len + (--index)) % this.len)
  }
}

const slide = new Slide('my-slider')

(2)解耦:  如果要加入底部的小点点进行联合控制,则会出现两份代码耦合在一起的现象,通过自定义事件的方式,进行解耦

// 在slideTo() 方法中加入如下自定义事件
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)

// 则可以在底部小点点的地方进行事件监听,从而更新状态
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';
})

// 在小点点事件onmouseover事件中,则可以通过直接调用Slide的slideTo()方法更新状态

前述两种方式都过于依赖内部代码,有没有一种可以扩展性更高的代码呢?这就要提到插件封装了~

(3)插件化: 将控制元素抽取成插件,使得插件和组件以依赖注入的方式建立联系

在Slide类中新增一个方法:

registerPlugins(...plugins){
  plugins.forEach(plugin => plugin(this));
}

在构造完Slide实例后,以slider.registerPlugins(pluginController, pluginPrevious, pluginNext);方式进行插件注册,从而实现较高程度的解耦

(4)模板化: 我们同样可以将HTML部分进行解耦,以render方式进行渲染

  • 在HTML部分就可以简化成如下内容:
<div id="my-slider" class="slider-list"></div>
  • 对于Silder类,本身有一个render方法如下:
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>`;
}
  • 可以通过在构造函数中进行render方法的调用,以如下的方式进行参数传递
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});
  • 对于Slider中的插件则以分成两个部分 ① render方法 ② action方法,在插件注册的过程中调用这两个方法

具体的调用方法还是如前面的插件化一样,只是多了一个render方法

(4)抽象化: 将Slide中能公用部分抽象出来成为一个Component通用组件,适用于更多的场景

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{
    constructor(id, opts, 3000) {
        super(id, opts);
        ...
    }
    ...
}

3. 过程抽象 - 函数式编程

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

常用的高阶函数

Once: 只执行一次的函数,依赖于闭包,返回一个新函数,原函数位于闭包作用域内,在第一次执行完之后,就将闭包作用域内的fn设为空,使其无法再次执行

/* * * * * *
-   Once   -
* * * * * */
function once(fn) {
    return function (...args) {
        if (fn) {
            let result = fn.apply(this, args); 
            fn = null;
            return result;
        }
    }
}

/* * * * * *
-   Test   -
* * * * * */
const foo = once(() => {
  console.log('bar');
});

foo();
foo();
foo();

Throttle: 节流函数,在规定时间内只让第一次生效,如果已经执行了该事件,在该段时间内不再触发该事件,比如鼠标的scroll

/* * * * * * * * *
-    Throttle    -
* * * * * * * * */
function Throttle (fn, delay) {
  let timer;
  return function (...args) {
    if (!timer) {
      fn.call(this, args)
      timer = setTimeout(() => {
        timer = null
      }, delay)
    }
  }
}

const buy = document.querySelector('#buyIt')
function buyFn (msg) {
  console.log("I'm buy it now! ", msg)
}

buy.addEventListener('click', Throttle(buyFn, 2000))

Debounce: 防抖函数,在规定时间内只让最后一次生效,每次触发都会引起重新计时,比如搜索框的防抖(只最最后一次变更进行搜索请求,自动保存(最后一次编辑变更,也有节流版本的自动保存))

/* * * * * * * * *
-    Debounce    -
* * * * * * * * */
function Debounce (fn, delay) {
  let timer;
  return function (...args) {
    // 如果还有定时器,那么就清除当前定时器
    if (timer) clearTimeout(timer)
    
    // 此时的上一个定时器不会执行,重新绑定定时器
    timer = setTimeout(() => {
      fn.call(this, args)
    }, delay)
  }
}

const buy = document.querySelector('#buyIt')
function buyFn (msg) {
  console.log("I'm buy it now! ", msg)
}

buy.addEventListener('click', Debounce(buyFn, 1000))

Consumer 和 isIterable (后续慢慢研究)

function consumer(fn, time){
  let tasks = [],
      timer;
  
  return function(...args){
    tasks.push(fn.bind(this, ...args));
    if(timer == null){
      timer = setInterval(() => {
        tasks.shift().call(this)
        if(tasks.length <= 0){
          clearInterval(timer);
          timer = null;
        }
      }, time)
    }
  }
}

function add(ref, x){
  const v = ref.value + x;
  console.log(`${ref.value} + ${x} = ${v}`);
  ref.value = v;
  return ref;
}

let consumerAdd = consumer(add, 1000);

const ref = {value: 0};
for(let i = 0; i < 10; i++){
  consumerAdd(ref, i);
}


// --------------

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

const setColor = iterative((el, color) => {
  el.style.color = color;
});

const els = document.querySelectorAll('li:nth-child(2n+1)');
setColor(els, 'red');

编程范式

声明式命令式

  • 声明封装功能细节,主要进行方法调用,以业务为主
  • 命令式则重功能细节,以过程为主

我们写代码要多以声明式的方式去实现,因为声明式的代码天生就具有可扩展性~