如何写好Javascript | 青训营笔记

90 阅读6分钟

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

一、本堂课重点内容:

本堂课的知识要点有哪些?

  • 写好Javascript的基本原则
  • 案例分析一
  • 案例分析二
  • 案例分析三
  • 写代码应该关注哪些要点?
  • Left-pad事件
  • 交通灯案例分析

二、详细知识点介绍:

  • 写好Javascript的基本原则
  1. 各司其职
  2. 组件封装
  3. 过程抽象
  • 案例分析一
    各司其职:HTML负责结构 CSS负责表现 JavaScript负责行为
    深夜食堂 写一段JS,控制一个网页,让它支持浅色和深色两种浏览模式。
    Version 1:
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 = '🌞';
  }
});

实行效果:

image.png

Version 2:

const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
  const body = document.body;
  if(body.className !== 'night') {
    body.className = 'night';
  } else {
    body.className = '';
  }
});

Version 3: HTML

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>深夜食堂</title>
</head>
<body>
  <input id="modeCheckBox" type="checkbox">
  <div class="content">
    <header>
      <label id="modeBtn" for="modeCheckBox"></label>
      <h1>深夜食堂</h1>
    </header>
    <main>
      <div class="pic">
        <img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg">
      </div>
      <div class="description">
        <p>
            这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈
            眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返 [6]  。
        </p>
      </div>
    </main>
  </div>
</body>
</html>

CSS

body, html {
  width: 100%;
  height: 100%;
  max-width: 600px;
  padding: 0;
  margin: 0;
  overflow: hidden;
}

body {
  box-sizing: border-box;
}

.content {
  height: 100%;
  padding: 10px;
  transition: background-color 1s, color 1s;
}

div.pic img {
  width: 100%;
}

#modeCheckBox {
  display: none;
}

#modeCheckBox:checked + .content {
  background-color: black;
  color: white;
  transition: all 1s;
}

#modeBtn {
  font-size: 2rem;
  float: right;
}

#modeBtn::after {
  content: '🌞';
}

#modeCheckBox:checked + .content #modeBtn::after {
  content: '🌜';
}

在版本一中,直接采用了Javascript改变了DOM元素的样式,表面上实现了需求,但是代码冗余复杂,不利于维护,易造成性能损耗。
在版本二中,采用class类来控制元素样式。
在版本三中,仅仅才用了HTML以及CSS代码就实现了功能,是简洁优雅的代码的象征。
由此总结来看:

  1. HTML/CSS/JS 应各司其责
  2. 应避免不必要的由 JS 直接操作样式
  3. 可以用 class 来表示状态
  4. 纯展示类交互寻求零 JS 方案
  • 案例分析二
    用原生 JS 写一个电商网站的轮播图,应该怎么实现?
    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>
            </div>

CSS:

#my-slider{
  position: relative;
  width: 790px;
  height: 340px;
}

.slider-list ul{
  list-style-type:none;
  position: relative;
  width: 100%;
  height: 100%;
  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;
}

.slide-list__control{
  position: relative;
  display: table;
  background-color: rgba(255, 255, 255, 0.5);
  padding: 5px;
  border-radius: 12px;
  bottom: 30px;
  margin: auto;
}

.slide-list__next,
.slide-list__previous{
  display: inline-block;
  position: absolute;
  top: 50%;
  margin-top: -25px;
  width: 30px;
  height:50px;
  text-align: center;
  font-size: 24px;
  line-height: 50px;
  overflow: hidden;
  border: none;
  background: transparent;
  color: white;
  background: rgba(0,0,0,0.2);
  cursor: pointer;
  opacity: 0;
  transition: opacity .5s;
}

.slide-list__previous {
  left: 0;
}

.slide-list__next {
  right: 0;
}

#my-slider:hover .slide-list__previous {
  opacity: 1;
}


#my-slider:hover .slide-list__next {
  opacity: 1;
}

.slide-list__previous:after {
  content: '<';
}

.slide-list__next:after {
  content: '>';
}

.slide-list__control-buttons,
.slide-list__control-buttons--selected{
  display: inline-block;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  margin: 0 5px;
  background-color: white;
  cursor: pointer;
}

.slide-list__control-buttons--selected {
  background-color: red;
}

JavaScript:

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 = {name: 'slider-list', data:[], cycle: 3000}){
    super(id, opts);
    this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
    this.cycle = opts.cycle || 3000;
    this.slideTo(0);
  }
  render(data){
    const content = data.map(image => `
      <li class="slider-list__item">
        <img src="${image}"/>
      </li>    
    `.trim());
    
    return `<ul>${content.join('')}</ul>`;
  }
  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);
  }
}

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){
    let controller = slider.container.querySelector('.slide-list__control');
    
    if(controller){
      let buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
      controller.addEventListener('mouseover', evt=>{
        var 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;
        let 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){
    let 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){
    let 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', {name: 'slide-list', data: ['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();

总结来看:

  1. 结构设计
  2. 展现效果
  3. 行为设计: API (功能) Event (控制流)
    重构:插件化
    解耦
    -将控制元素抽取成插件
    -插件与组件之间通过依赖注入方式建立联系
 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();
              });
            }  
          }

重构:模板化

  • 将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>`;
            }
            ...
          }

组件框架

  • 将组件通用模型抽象出来
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. 抽象化(组件框架)
  • 案例分析三

过程抽象

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

image.png

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

const list = document.querySelector('ul');
const buttons = list.querySelectorAll('button');
// buttons.forEach((button) => {
//   button.addEventListener('click', once((evt) => {
//     const target = evt.target;
//     target.parentNode.className = 'completed';
//     setTimeout(() => {
//       list.removeChild(target.parentNode);
//     }, 2000);
//   }));
// });

const foo = once(() => {
  console.log('bar');
});

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

image.png

操作次数限制

  • 一些异步交互
  • 一次性的HTTP请求

Once

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

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

高阶函数

  • 以函数作为参数

  • 以函数作为返回值

  • 常用于作为函数装饰器

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

常用高阶函数

编程范式

总结:

  • 过程抽象 / HOF / 装饰器

  • 命令式 / 声明式

  • 写代码应该关注哪些要点?
    1.风格
    2.效率
    3.约定
    4.使用场景
    5.设计

  • Left-pad事件

 function leftpad(str, len, ch) {
                str = String(str);
                var i = -1;
                if (!ch && ch !== 0) ch = ' ';
                len = len - str.length;
                while (++i < len) {
                    str = ch + str;
                }
                return str;
            } 
  • NPM 模块粒度
  • 代码风格
  • 代码质量/效率
 function leftpad(str, len, ch) {
                str = "" + str;
                const padLen = len - str.length;
                if(padLen <= 0) {
                  return str;
                }
                return (""+ch).repeat(padLen)+str;
            } 

分析:代码更简洁 效率提升

  • 交通灯案例分析
    Version 1
    HTML:
<ul id="traffic" class="wait">
  <li></li>
  <li></li>
  <li></li>
  <li></li>
  <li></li>
</ul>

CSS:

#traffic {
  display: flex;
  flex-direction: column;
}

#traffic li{
  list-style: none;
  width: 50px;
  height: 50px;
  background-color: gray;
  margin: 5px;
  border-radius: 50%;
}

#traffic.s1 li:nth-child(1) {
  background-color: #a00;
}

#traffic.s2 li:nth-child(2) {
  background-color: #aa0;
}

#traffic.s3 li:nth-child(3) {
  background-color: #0a0;
}

#traffic.s4 li:nth-child(4) {
  background-color: #a0a;
}

#traffic.s5 li:nth-child(5) {
  background-color: #0aa;
}

JavaScript:

const traffic = document.getElementById('traffic');

(function reset(){
  traffic.className = 's1';
  
  setTimeout(function(){
      traffic.className = 's2';
      setTimeout(function(){
        traffic.className = 's3';
        setTimeout(function(){
          traffic.className = 's4';
          setTimeout(function(){
            traffic.className = 's5';
            setTimeout(reset, 1000)
          }, 1000)
        }, 1000)
      }, 1000)
  }, 1000);
})();

Version 2(数据抽象)
JavaScript:

const traffic = document.getElementById('traffic');

const stateList = [
  {state: 'wait', last: 1000},
  {state: 'stop', last: 3000},
  {state: 'pass', last: 3000},
];

function start(traffic, stateList){
  function applyState(stateIdx) {
    const {state, last} = stateList[stateIdx];
    traffic.className = state;
    setTimeout(() => {
      applyState((stateIdx + 1) % stateList.length);
    }, last)
  }
  applyState(0);
}

start(traffic, stateList);

Version 3(过程抽象)
JavaScript:

const traffic = document.getElementById('traffic');

function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function poll(...fnList){
  let stateIndex = 0;
  
  return async function(...args){
    let fn = fnList[stateIndex++ % fnList.length];
    return await fn.apply(this, args);
  }
}

async function setState(state, ms){
  traffic.className = state;
  await wait(ms);
}

let trafficStatePoll = poll(setState.bind(null, 'wait', 1000),
                            setState.bind(null, 'stop', 3000),
                            setState.bind(null, 'pass', 3000));

(async function() {
  // noprotect
  while(1) {
    await trafficStatePoll();
  }
}());

Version 4(异步+函数式)
JavaScript:

const traffic = document.getElementById('traffic');

function wait(time){
  return new Promise(resolve => setTimeout(resolve, time));
}

function setState(state){
  traffic.className = state;
}

async function start(){
  //noprotect
  while(1){
    setState('wait');
    await wait(1000);
    setState('stop');
    await wait(3000);
    setState('pass');
    await wait(3000);
  }
}

start();

三、课后个人总结:

Javascript语言内容还是非常丰富的,在本次课程中不可能讲完所有的语言基础知识以及写好该语言的全部方法。所以需要课下自己进行更多的学习,写代码终究还是实践性很强的一门课,需要自己多多实践,才能提升自身能力。

四、引用参考:

青训营 跟着月影学 JavaScript (上)
青训营 ‬⁡⁣跟着月影学 JavaScript(下)