跟着月影学JavaScript | 青训营笔记

89 阅读6分钟

跟着月影学JavaScript | 青训营笔记

这是我参加【第四届青训营】笔记创作活动的第5天

今天又重新理了理Javascript的内容,并记录下笔记。

本堂课,月影老师向我们介绍了写好js的一些原则,主要分为以下几方面:

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

一、各司其职

写一段JS,控制一个网页,让它支持浅色和深色两种浏览模式。 如果是你来实现,你会怎么做?

版本一:

   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做了CSS的工作,没有做到各司其职,不利于维护修改。

版本二:

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

通过指定类名来切换样式,做到了CSS和JavaScript的职能分离。

版本三:

纯CSS实现,通过使用伪类选择器来完成功能。

首先在HTML中嵌入一个类型为checkbox的input组件,设置为display: none;

其次,通过使用label组件的for属性来修改这个checkbox的状态

最后,在CSS中指定一个状态性伪类+兄弟选择器,在checkbox的状态为checked的情况下选中后面类名为content的组件修改样式,从而实现暗黑模式。

选中暗黑模式时,由于该选择器最特殊,因此该选择器指定的样式会被选择。

另外通过CSS的transition属性可以实现一个简单的动画。

 <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>
 #modeCheckBox {
     display: none;
   }
 ​
   #modeCheckBox:checked + .content {
     background-color: black;
     color: white;
     transition: all 1s;
   }

深夜食堂——结论

img

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

二、组件封装

组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性、复用性。

例子:用原生js写一个电商网站的轮播图,应该怎么实现?

结构:HTML

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

 <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

  • 使用 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;
   }

行为:JS

img

 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
     }
     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';
       }
     }
     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);  
     }
   }
 ​
   const slider = new Slider('my-slider');
   slider.slideTo(3);

行为:控制流

  • 使用自定义事件来解耦。
 <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>
 const detail = {index: idx}
   const event = new CustomEvent('slide', {bubbles:true, detail})
   this.container.dispatchEvent(event)

总结:基本方法

  • 结构设计

  • 展现效果

  • 行为设计

    1. API (功能)
    2. 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模板化,更易于扩展

90e87acfda88ef5cfb36f2206ec7c85.jpg

 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>`;
     }
     ...
   }
抽象

将组件通用模型抽象出来

cc738d3ec931e08f05004e708bffc50.jpg

 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. 抽象化(组件框架)

三、过程抽象

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

例子:操作次数限制

  • 一些异步交互
  • 一次性的HTTP请求
 const list = document.querySelector('ul');
   const buttons = list.querySelectorAll('button');
   buttons.forEach((button) => {
     button.addEventListener('click', (evt) => {
       const target = evt.target;
       target.parentNode.className = 'completed';
       setTimeout(() => {
         list.removeChild(target.parentNode);
       }, 2000);
     });
   });

高阶函数

HOF

  • 以函数作为参数
  • 以函数作为返回值
  • 常用于作为函数装饰器
 function HOF0(fn) {
     return function(...args) {
       return fn.apply(this, args);
     }
   }

常用高阶函数

HOF

为什么要使用高阶函数?

-
纯函数没有副作用 是一个规范的过程不会改变外部状态方便测试
非纯函数有副作用 是一个不规范的过程会改变外部状态测试开始需要初始化环境 测试结束需要销毁环境 不方便测试

使用高阶函数可以大大减少我们使用非纯函数的可能性

编程范式

当年的Leftpad事件

   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;
   } 
  • 代码更简洁
  • 效率提升
性能更好
/*! https://mths.be/repeat v1.0.0 by @mathias */

  'use strict';

  var RequireObjectCoercible = require('es-abstract/2019/RequireObjectCoercible');
  var ToString = require('es-abstract/2019/ToString');
  var ToInteger = require('es-abstract/2019/ToInteger');

  module.exports = function repeat(count) {
    var O = RequireObjectCoercible(this);
    var string = ToString(O);
    var n = ToInteger(count);
    // Account for out-of-bounds indices
    if (n < 0 || n == Infinity) {
      throw RangeError('String.prototype.repeat argument must be greater than or equal to 0 and not be Infinity');
    }

    var result = '';
    while (n) {
      if (n % 2 == 1) {
        result += string;
      }
      if (n > 1) {
        string += string;
      }
      n >>= 1;
    }
    return result;
  };
  
  
  
/**
   * String.prototype.repeat() polyfill
   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat#Polyfill
  */
if (!String.prototype.repeat) {
    String.prototype.repeat = function(count) {
      'use strict';
      if (this == null)
        throw new TypeError('can\'t convert ' + this + ' to object');

      var str = '' + this;
      // To convert string to integer.
      count = +count;
      // Check NaN
      if (count != count)
        count = 0;

      if (count < 0)
        throw new RangeError('repeat count must be non-negative');

      if (count == Infinity)
        throw new RangeError('repeat count must be less than infinity');

      count = Math.floor(count);
      if (str.length == 0 || count == 0)
        return '';

      // Ensuring count is a 31-bit integer allows us to heavily optimize the
      // main part. But anyway, most current (August 2014) browsers can't handle
      // strings 1 << 28 chars or longer, so:
      if (str.length * count >= 1 << 28)
        throw new RangeError('repeat count must not overflow maximum string size');

      var maxCount = str.length * count;
      count = Math.floor(Math.log(count) / Math.log(2));
      while (count) {
        str += str;
        count--;
      }
      str += str.substring(0, maxCount - str.length);
      return str;
    }
  }