JavaScript | 青训营笔记

72 阅读3分钟

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

JS原则

各司其责:HTML、CSS和JS职能分离

组件封装:正确性、扩展性、复用性

过程抽象:函数式编程思想

各司其责

Q:夜间模式的实现

职能分离原则,采用JS控制类名添加

📌 纯CSS实现

总结

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

组件封装

📌 控制UI组件

  • 轮播图

    1. 使用绝对定位使多张图片重叠在一起
    2. 轮播图切换的状态使用修饰符(modifier)
    3. 轮播图的切换动画使用 CSS transition
<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{
    position: absolute;
    transition: opacity 1s;
    opacity: 0;
    text-align: center;
  }

  .slider-list__item--selected{
    transition: opacity 1s;
    opacity: 1;
  }
  • 组件封装

/*
- Slider
	- +getSelectedItem()
	- +gerSelectedItemIndex()
	- +slideTo()
	- +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 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:控制流
  • 代码耦合度高 → JS插件化+依赖注入

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();
      });
    }  
  }
  • 数据控制视图 → 模板化+render函数
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>`;
    }
    ...
  }
  • 组件框架 → 抽象:通用组件模型抽象出来

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

总结

  • 结构设计、展现效果、行为设计

  • 三次重构,逐步深入,不改变各司其责的思想

    • 插件化
    • 模板化
    • 抽象化(组件框架)

过程抽象

📌 处理局部细节(典例:react hooks)

高阶函数

function.png

关于高阶函数的介绍www.jianshu.com/p/acb132ec9…

HOF(Higher-Order Function),可以把函数作为参数,或是将函数作为返回值的函数。

📌 后续传递风格(Continuation Passing Style):函数装饰器

HOF.png

常用高阶函数:

  • Once:操作次数限制,常用于异步交互和HTTP请求
function once(fn) {
    return function(...args) {
      if(fn) {
        const ret = fn.apply(this, args);
        fn = null;
        return ret;
      }
    }
  }
  • Debounce:防抖,触发事件停止后一段时间才执行
var i = 0;
setInterval(function(){
  bird.className = "sprite " + 'bird' + ((i++) % 3);
}, 1000/10);

function debounce(fn, dur){
  dur = dur || 100;
  var timer;
  return function(...arguments){
    clearTimeout(timer);
		// 不再触发函数时,间隔dur之后就会执行函数
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, dur);
  }
}

document.addEventListener('mousemove', debounce(function(evt){
  var x = evt.clientX,
      y = evt.clientY,
      x0 = bird.offsetLeft,
      y0 = bird.offsetTop;
  
  console.log(x, y);
  
  var a1 = new Animator(1000, function(ep){
    bird.style.top = y0 + ep * (y - y0) + 'px';
    bird.style.left = x0 + ep * (x - x0) + 'px';
  }, p => p * p);
  
  a1.animate();
}, 100));
  • Throttle:节流,触发事件大于等于间隔时间触发
function throttle(fn, time = 500){
  let timer;
  return function(...args){
    if(timer == null){
			// 让函数正常执行
      fn.apply(this,  args);
			// 然后封印函数的触发time时间
      timer = setTimeout(() => {
        timer = null;
      }, time)
    }
  }
}

btn.onclick = throttle(function(e){
  circle.innerHTML = parseInt(circle.innerHTML) + 1;
  circle.className = 'fade';
  setTimeout(() => circle.className = '', 250);
});
  • Consumer
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);
}
  • 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]);
  }
}

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

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

为什么要用高阶函数解决这些问题?

什么是纯函数?

pure function is a function which:

  • Given the same input, always returns the same output.
  • Produces no side effects.

个人理解:无副作用(例如用闭包来清理、隔离变量)的有限状态机

纯函数用什么好处?

可预测(给定输入就知道输出,可预测的状态机);

测试难度高,维护性差;

开销小。

📌 高阶函数的输入输出是确定的,return只由输入fn决定。便于测试。

编程范式:命令式 vs. 声明式

programming.png

命令式强调怎么做(HOW)

每多一种情况,就需要增加逻辑分支

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

📌 声明式相比于命令式具有天然的可扩展的优势

this指针问题

原型继承和类继承的区别:构造函数调用。

写代码应该关注什么?

  • 风格
  • 效率
  • 约定
  • 使用场景
  • 设计