前端进阶 | 如何写好JS(含实例+完整代码)

1 篇文章 0 订阅
1 篇文章 0 订阅

写好JS的原则

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

一、各司其责

实例:深夜食堂

功能:夜间模式切换
image.png
效果
QQ录屏20230116142900 00_00_00-00_00_30.gif
版本一 - 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

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

问题:版本二符合各司其责的原则。版本一用JS做了CSS应该做的事情,混在一起写不方便后续其他人的修改。

版本三 - 只用纯CSS控制样式,不使用JS。

HTML

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

CSS

          #modeCheckBox {
            display: none;
          }

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

思考结论
image.png

二、组件封装

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

image.png
效果:
QQ录屏20230116142706 00_00_00-00_00_30.gif

1、结构HTML

HTML,轮播图是一个典型的列表结构,可以用无序列表ul>li*4来实现。

HTML

            <div id="my-slider" class="slider-list">
              <ul>
                <li class="slider-list__item--selected">
                  <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/490a9fda06c042b2b00d4505b324b996~tplv-k3u1fbpfcp-zoom-1.image"/>
                </li>
                <li class="slider-list__item">
                  <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b30362f1e345488a82fb4562b85aca89~tplv-k3u1fbpfcp-zoom-1.image"/>
                </li>
                <li class="slider-list__item">
                  <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/17a15493fd0f46f48fe51244098a8b8b~tplv-k3u1fbpfcp-zoom-1.image"/>
                </li>
                <li class="slider-list__item">
                  <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3d4b18ffcb5447d192f264548ee0ff50~tplv-k3u1fbpfcp-zoom-1.image"/>
                </li>
              </ul>
            </div>
2、表现CSS
  • 使用CSS结对定位将图片重叠在同一位置
  • 轮播图切换的状态使用修饰符(modifier)
  • 轮播图的切换动画使用CSS transition

CSS

          #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;
          }
3、行为JS

API的设计应保证原子操作,职能单一,满足灵活性

行为:API
  • Slider
  • +getSelectedItem()
  • +getSelectedItemIndex()
  • +slideTo()
  • +slideNext()
  • +slidePrevious()
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);
//setInterval(()=>{
//    slider.slideNext();
//  },2000);
行为:控制流
  • 使用自定义事件来解耦。

上面的HTML结构中,使用a元素分别表示上一张和下一张,span*4 表示底部的四个小圆点。

HTML

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

设计需求分析:

  1. 图片循环播放,每张停留若干时间
  2. 点击左右箭头可以切换
  3. 鼠标悬浮在底部小圆点上时会跳到对应图片
  4. 小圆点也会随着图片滚动

然后通过setInterval()实现循环播放,间隔为3秒:

JS-控制完整实现

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

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

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>
4、总结:基本方法
  • 结构设计
  • 展现效果
  • 行为设计
  1. API (功能)
  2. Event (控制流)
5、重构:插件化

在JS代码中,一个方法一般来说最多只能有15行代码,超过了就需要重构。为了便于代码的修改和优化,引入了插件化。

解耦
  • 将控制元素抽取成插件
  • 插件与组件之间通过依赖注入方式建立联系

插件化后的JS完整版代码

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

这样写的好处是,可以不修改某一部分的代码,如果需要增加新的组件,只需要单独写一个function插件,就可以添加进去。
比如增加一个随机抽取广告图片的按键:
image.png
QQ录屏20230116142621 00_00_00-00_00_30.gif
HTML增加代码

<button id="randomGet">手气不错</button>

JS部分增加代码

function pluginRandomGet(slider){
randomGet.addEventListener('click',evt => {
const idx =Math.floor(slider.items.length*Math.random());
slider.stop();
slider.slideTo(idx);
slider.start();
})
}

并将这个新写的插件function在结尾new Slider中调用调用。代码如下:

const slider = new Slider('my-slider');
slider.registerPlugins(pluginController, pluginPrevious, pluginNext,pluginRandomGet);
slider.start();

即可以以插件的形式对组件功能进行扩充,而不用修改原始组件的代码。

6、重构:模板化

将HTML模板化,更易于拓展。
image.png
JS模板化完整版

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

如果某一部分比如删减pluginController功能,只需要把最后slider.registerPlugins(pluginController, pluginPrevious, pluginNext);中的pluginController注释掉即可,非常简单。
HTML中只需要简单的定义一行即可,其余的均在JS中模板化。

<div id="my-slider" class="slider-list"></div>

但是这是优化的终点吗?
不是。

7、组件框架

抽象:
将组件通用模型抽象出来。
image.png
组件化后的final版本

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

支持定义一个组件,组件里注册若干插件。

这种抽象方法的好处是,组件设计都很简单,任何一个组件都是由组件和控制插件组成。

不好的地方是:我们的这种设计没有考虑嵌套,有一些组件是可以嵌套的,子组件是可以作为父组件的插件来使用的,但是在上述代码中我们是将component和plugin是分开的。

当然我们可以设计一种更通用的模型,可以将component和plugin组合起来,构成一个subcomponent,有子组件,子组件本身也可以做父组件插件来使用的组件框架,可以任意拓展。

8、总结
  • 组件设计的原则:封装性、正确性、扩展性、复用性

  • 实现组件的步骤:结构设计、展现效果、行为设计

  • 三次重构

    1. 插件化
    2. 模板化
    3. 抽象化(组件框架)

后续的进一步优化:

  1. 考虑父子组件
  2. CSS组件化

三、过程抽象

  • 用来处理局部细节控制的一些方法
  • 函数式编程思想的基础应用
    image.png
    为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象

实例:操作次数限制

  • 一些异步交互
  • 一次性的HTTP请求
    QQ录屏20230116142313 00_00_00-00_00_30.gif
    完整实现
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  <ul>
    <li><button></button><span>任务一:学习HTML</span></li>
    <li><button></button><span>任务二:学习CSS</span></li>
    <li><button></button><span>任务三:学习JavaScript</span></li>
  </ul>
</body>
</html>
ul {
  padding: 0;
  margin: 0;
  list-style: none;
}

li button {
  border: 0;
  background: transparent;
  cursor: pointer;
  outline: 0 none;
}

li.completed {
  transition: opacity 2s;
  opacity: 0;
}

li button:before {
  content: '☑️';
}

li.completed button:before {
  content: '✅';
}
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();
1、高阶函数

HOF

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

常用高阶函数

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

应用:(代码见上文)
QQ录屏20230116142313 00_00_00-00_00_30.gif

Throttle 节流函数

限制频率

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

应用:限制频率500ms记录一次
QQ录屏20230116145352 00_00_00-00_00_30.gif
完整实现:

每500毫秒可记录一次
<button id="btn">点我</button>
<div id="circle">0</div>
#circle {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background-color: red;
  line-height: 50px;
  text-align: center;
  color: white;
  opacity: 1.0;
  transition: opacity .25s;
}

#circle.fade {
  opacity: 0.0;
  transition: opacity .25s;
}
function throttle(fn, time = 500){
  let timer;
  return function(...args){
    if(timer == null){
      fn.apply(this,  args);
      timer = setTimeout(() => {
        timer = null;
      }, time)
    }
  }
}

btn.onclick = throttle(function(e){
  circle.innerHTML = parseInt(circle.innerHTML) + 1;
  circle.className = 'fade';
  setTimeout(() => circle.className = '', 250);
});
debounce 防抖
function debounce(fn, dur){
  dur = dur || 100;
  var timer;
  return function(){
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, dur);
  }
}

应用:抖动时不保存信息,静止1s后保存信息。
1673852595046.gif
完整实现:

<script src="https://s1.qhres2.com/!bd39e7fb/animator-0.2.0.min.js"></script>
<div id="bird" class="sprite bird1"></div>
html, body {
  margin:0;
  padding:0;
}

.sprite {
  display:inline-block; overflow:hidden; 
  background-repeat: no-repeat;
  background-image:url(https://p1.ssl.qhimg.com/d/inn/0f86ff2a/8PQEganHkhynPxk-CUyDcJEk.png);
}

.bird0 {width:86px; height:60px; background-position: -178px -2px}
.bird1 {width:86px; height:60px; background-position: -90px -2px}
.bird2 {width:86px; height:60px; background-position: -2px -2px}

#bird{
  position: absolute;
  left: 100px;
  top: 100px;
  transform: scale(0.5);
  transform-origin: -50% -50%;
}
var i = 0;
setInterval(function(){
  bird.className = "sprite " + 'bird' + ((i++) % 3);
}, 1000/10);

function debounce(fn, dur){
  dur = dur || 100;
  var timer;
  return function(){
    clearTimeout(timer);
    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));
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)
    }
  }
}

应用1:每隔一个时间,调用一次,同步调用变异步调用。
QQ录屏20230116152440.gif
完整实现

<div id="app"></div>
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);
}

应用2:连续快速点击,每隔800ms显示一次。延时调用效果。
QQ录屏20230116153013 00_00_00-00_00_30.gif
完整实现

<div id="main">
  <button id="btn">Hit</button>
  <span id="count">+0</span>
</div>
#main {
  padding-top: 20px;
  font-size: 26px;
}

#btn {
  font-size: 30px;
  border-radius: 15px;
  border: solid 3px #fa0;
}

#count {
  position: absolute;
  margin-left: 6px;
  opacity: 1.0;
  transform: translate(0, 10px);
}

#count.hit {
  opacity: 0.1;
  transform: translate(0, -20px);
  transition: all .5s;
}
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)
    }
  }
}

btn.onclick = consumer((evt)=>{
  let t = parseInt(count.innerHTML.slice(1)) + 1;
  count.innerHTML = `+${t}`;
  count.className = 'hit';
  let r = t * 7 % 256,
      g = t * 17 % 128,
      b = t * 31 % 128;
  
  count.style.color = `rgb(${r},${g},${b})`.trim();
  setTimeout(()=>{
    count.className = 'hide';
  }, 500);
}, 800)
iterative 可迭代方法
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]);
  }
}

应用:批量操作-将奇数行变成红色
image.png

<ul>
  <li>a</li>
  <li>b</li>
  <li>c</li>
  <li>d</li>
  <li>e</li>
  <li>f</li>
  <li>g</li>
</ul>
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');
2、编程范式
  • 命令式与声明式

image.png
应用:
QQ录屏20230116160939 00_00_00-00_00_30.gif
命令式编程:

<div id="switcher" class="on"></div>
#switcher {
  display: inline-block;
  background-color: black;
  width: 50px;
  height: 50px;
  line-height: 50px;
  border-radius: 50%;
  text-align: center;
  cursor: pointer;
}

#switcher.on {
  background-color: green;
}

#switcher.off {
  background-color: red;
}

#switcher.on:after {
  content: 'on';
  color: white;
}

#switcher.off:after {
  content: 'off';
  color: white;
}
switcher.onclick = function(evt){
  if(evt.target.className === 'on'){
    evt.target.className = 'off';
  }else{
    evt.target.className = 'on';
  }
}

声明式编程:

<div id="switcher" class="on"></div>
#switcher {
  display: inline-block;
  background-color: black;
  width: 50px;
  height: 50px;
  line-height: 50px;
  border-radius: 50%;
  text-align: center;
  cursor: pointer;
}

#switcher.on {
  background-color: green;
}

#switcher.off {
  background-color: red;
}

#switcher.on:after {
  content: 'on';
  color: white;
}

#switcher.off:after {
  content: 'off';
  color: white;
}
function toggle(...actions){
  return function(...args){
    let action = actions.shift();
    actions.push(action);
    return action.apply(this, args);
  }
}

switcher.onclick = toggle(
  evt => evt.target.className = 'off',
  evt => evt.target.className = 'on'
);

对比命令式和声明式的JS代码发现,命令式如果添加拓展分支状态需要更改代码,添加新的if else逻辑;而声明式编程添加更多的分支只需要改个状态就可以了。

比如添加一种warn状态,声明式编程做如下改动:

CSS部分:

#switcher.warn {
  background-color: yellow;
}
#switcher.warn:after {
  content: 'warn';
  color: black;

JS部分添加一种状态即可:

switcher.onclick = toggle(
  evt => evt.target.className = 'off',
  evt => evt.target.className = 'on',
  evt => evt.target.className = 'warn'
);

QQ录屏20230116162441 00_00_00-00_00_30.gif
声明式编程天然比命令式编程具有较强的可拓展性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值