【青训营】跟着月影学JS笔记

304 阅读5分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

笔记大纲

  1. HTML、CSS、JS的职责分离
  2. 组件封装,以轮播图为例
  3. 过程抽象、高阶函数与编程范式

01. 各司其职

分层

HTML-> 结构

CSS -> 表现

Javascript -> 行为

// 版本一
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 = '🌞';
    }
});

//js修改了样式,但不易于理解整体是做了什么事情
//版本二
const btn = document.getElementById('modeBtn');
btn.addEventListener('click',(e) => {
	const body = document.body;
    if(body.className !== 'night' ){
    	body.className = 'night';
    }else {
    body.className = '';
    }
});
//使用描述性className去切换,js和css样式各司其职
//版本三
//html部分
<body>
	<input id='modeCheckBox' type='checkbox'>
    <div class='content'>
    	<header>
        	<label id='modeBtn' for='modeCheckBox'></label>
            <h1>深夜食堂</h1>
        </header>
        <main>
        </main>
    </div>
</body>

//纯CSS
#modeCheckBox:checked + .content {
	background-color:black;
    color: white;
    transition: all 1s;
}
//缺点没用js所以状态无法保存
  • 避免js直接修改DOM样式
  • 可以使用className修改
  • 纯展示类交互寻求零JS方案

02. 组件封装

组件是指Web页面上抽取出来一个个包含模版、功能和样式的单元。好的组件应该有封装性、正确性、扩展性、复用性。

例子 用原生JS写一个电商网站的轮播图

<div id='my-slider' class='slider-list'>
	<ul>
  	<li class='slider-list__item--selected'><img src='1.img'/></li>
    <li class='slider-list__item'><img src='2.img'/></li>
    <li class='slider-list__item'><img src='3.img'/></li>
    <li class='slider-list__item'><img src='4.img'/></li>
  </ul>
</div>
//使用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 1s;
  opacity:0;
  text-align:center;
}
.slider-list__item--selected{
	transition:opacity  1s;
  opacity:1;
}

API(功能)设计应保证原子操作,职责单一,满足灵活性。

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
  }
  getSelectdItemIndex(){
  	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 = getSelectdItemIndex();
    const nextIdx = (currentIdx+1)%this.item.length;
    this.slideTo(nextIdx)
  }
  slidePrevious(){
    const currentIdx = getSelectdItemIndex();
    const previousIdx = (currentIdx+1)%this.item.length;
    this.slideTo(previousIdx)
  }
}

Event 控制流:使用自定义事件来解耦

解耦

  • 将控制元素抽取成插件
  • 组件和插件之间使用依赖注入
  • 将HTML模版化,更易于扩展
  • 将通用的组件模型抽象出来 (组件框架)
//https://code.h5jun.com/weru/3/edit?html,css,js,output
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();

//js解耦了但HTML还未解耦

将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>`;
  }
  registerPlugins(...plugins){
    plugins.forEach(plugin => {
      const pluginContainer = document.createElement('div');
      pluginContainer.className = '.slider-list__plugin';
      pluginContainer.innerHTML = plugin.render(this.options.images);
      this.container.appendChild(pluginContainer);
      
      plugin.action(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';
    }
    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(){
    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);
  }
}

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

const pluginPrevious = {
  render(){
    return `<a class="slide-list__previous"></a>`;
  },
  action(slider){
    const previous = slider.container.querySelector('.slide-list__previous');
    if(previous){
      previous.addEventListener('click', evt => {
        slider.stop();
        slider.slidePrevious();
        slider.start();
        evt.preventDefault();
      });
    }  
  }
};

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

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.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();

03. 过程抽象

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

高阶函数 HOF

常用高阶函数

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

Once

操作次数限制

  • 一些异步操作
  • 一次性的HTTP请求

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

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

Throttle

function throtte(fn,time=500){
	let timer;
  return function(...args){
  	if(timer == null){
    	fn.apply(this,args);
      timer = setTimeout(()=>{
      	timer = null;
      },time)
    }	
  }
}

Debunce

function debounce(fn,dur){
	dur = dur ||100;
  var timer;
  return function(){
  	clearTimeout(timer);
    timer = setTimeout(()=>{
    	fn.apply(this,arguments);
    },dur);
  }
}

Consumer

Iterative

纯函数

  1. 没有副作用
  2. 幂等性 (在任何时候调用,结果固定)

无侵入式代码

编程范式

命令式和声明式

function toggle(...actions){
	return function(...args){
  	let action = actions.shift();
    actions.push(action);
    return action.apply(this,...args);
  }
}

风格统一:tab、分号、eslint、团队约定

执行效率:结合使用场景

洗牌算法

const card = [0,1,2,3,4,5,6,7,8,9];
function draw(cards){
	const c = [...cards];
  for(let i = c.length;i>0;i--){
  const pIdx = Math.floor(Math.random()*i);
    [c[pIdx],c[i-1]] = [c[i-1],c[pIdx]];
    yield c[i-1];
  }
}
const result =draw(cards);
console.log([...result]);

分红包算法

  1. 切西瓜法,每次切最大的或者第二大的红包
  2. 洗牌法,10个红包找9个位置

后记

  这篇笔记也是比较长了,重新温习了一下月影老师上课的内容,学习到了很多厉害的东西,组件封装和高阶函数那块内容都让我影响深刻。正如老师上课的标题“如何写好JS”,这节课的内容大部分是在教如何在编写JS代码时抽象与思考。职责分配、组件封装、过程抽象、编程范式、算法等等,只待之后的学习编码过程中慢慢思考与实践了。To be continued...