第六届360前端星计划_JavaScript 从入门到放弃

主讲人:月影

思考:如何写“好” JavaScript

——前端工程师的最佳实践套路 模式
什么才是好的 JS 代码?

一、各司其职

JavaScript :行为
CSS :表现
HTML:结构
尽量做到职责分离
在这里插入图片描述

  1. 关灯吃面:版本1
    黑夜与白天的切换
    HTML代码:
<div id="main">
  <div class="pic">
    <img src="https://p4.ssl.qhimg.com/t01e932bf06236f564f.jpg">
  </div>
  <div class="content">
    <pre>
今天回到家,
煮了点面吃,
一边吃面一边哭,
泪水滴落在碗里,
没有开灯。
    </pre>
  </div>
  <a id="light" href="###"> </a>
</div>

CSS代码:

html,body {
  margin: 0;
  padding: 20px;
  width: 100%;
  height: 100%;
}

#main {
  position: relative;
}

.pic {
  float: left;
  margin-right: 20px;
}

.content {
  font-weight: bold;
  font-size: 1.5em;
}

a#light {
  border: none;
  width: 25px;
  height: 25px;
  border-radius: 50%;
  position: absolute;
  left: 10px;
  top: 10px;
  cursor: pointer;
  background: red;
}
light.onclick = function(evt) {
  if(light.style.backgroundColor !== 'green'){
    document.body.style.backgroundColor = '#000';
    document.body.style.color = '#fff';
    light.style.backgroundColor = 'green';
  }else{
    document.body.style.backgroundColor = '';
    document.body.style.color = '';
    light.style.backgroundColor = '';    
  }
}

运行效果:

在这里插入图片描述
点击红色按钮后:
在这里插入图片描述

  1. 讨论:这个版本有哪些问题?
    违反了各司其职的原则。
    用JS做了本该CSS负责的事情。

  2. 关灯吃面:版本2

<div id="main" class="light-on">          /*添加了calss*/
  <div class="pic">
    <img src="https://p4.ssl.qhimg.com/t01e932bf06236f564f.jpg">
  </div>
  <div class="content">
    <pre>
今天回到家,
煮了点面吃,
一边吃面一边哭,
泪水滴落在碗里,
没有开灯。
    </pre>
  </div>
  <a id="lightButton" href="###"> </a>
</div>
html,body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
}

#main {
  position: relative;
  padding: 20px;
  width: 100%;
  height: 100%;
  transition: all .5s;           /*添加了过渡动画*/
}

#main.light-off {                 /*添加了开关灯的样式*/
  background-color: #000;
  color: #fff;
}

#main.light-on {                 
  background-color: #fff;
  color: #000;
}

.pic {
  float: left;
  margin-right: 20px;
}

.content {
  font-weight: bold;
  font-size: 1.5em;
}

#lightButton {
  border: none;
  width: 25px;
  height: 25px;
  border-radius: 50%;
  position: absolute;
  left: 30px;
  top: 30px;
  cursor: pointer;
  background: red;
}

#main.light-off #lightButton {
  background: green;
}

lightButton.onclick = function(evt) {
  if(main.className === 'light-on'){
    main.className = 'light-off';
  }else{
    main.className = 'light-on';
  }
}

  1. 讨论:这个版本做了哪些改进?
    各司其职:
    1)HTML代码添加了class=“light-on”
    2)CSS代码添加了开关灯的样式、过渡动画。
    3)JS代码变简单,易理解。
  2. 还有其他思路?
    由于此案例是纯展示的,可以用纯CSS实现效果,不用写JS代码。
    HTML:
<input id="light" type="checkbox"></input>    /*CSS中隐藏了*/
<div id="main">
  <div class="pic">
    <img src="https://p4.ssl.qhimg.com/t01e932bf06236f564f.jpg">
  </div>
  <div class="content">
    <pre>
今天回到家,
煮了点面吃,
一边吃面一边哭,
泪水滴落在碗里,
没有开灯。
    </pre>
  </div>
  <label for="light">                          /*for指定ID元素*/
    <span id="lightButton"> </span>
  <label>
</div>

css

html,body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
}

#light {
  display: none;
}

#main {
  position: relative;
  padding: 20px;
  width: 100%;
  height: 100%;
  background-color: #fff;
  color: #000;
  transition: all .5s;
}

#light:checked + #main {              /*兄弟节点选择器*/
  background-color: #000;
  color: #fff;
}

.pic {
  float: left;
  margin-right: 20px;
}

.content {
  font-weight: bold;
  font-size: 1.5em;
}

#lightButton {
  border: none;
  width: 25px;
  height: 25px;
  border-radius: 50%;
  position: absolute;
  left: 30px;
  top: 30px;
  cursor: pointer;
  background: red;
}

#light:checked+#main #lightButton {
  background: green;
}

  1. 讨论:二、三两版什么时候用?
    考虑到浏览器版本比较老的时候,用方案二好。
    移动端的话用方案三好,易维护。

二、复杂 UI 组件的设计

例子:京东轮播图
特点:图片定时轮换,点击小圆点和左右按钮,图片会按顺序轮换。
讨论:这样的 UI 组件如何去写?

  1. 步骤1:结构设计
    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;
}

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


  • 思考
    1)图片结构是一个列表型结构,所以主体用 ul
    2)使用 css 绝对定位将图片重叠在同一个位置
    3)轮播图切换的状态使用修饰符(modifier)
    4)轮播图的切换动画使用 css transition
  1. 步骤2:API 设计
    getSelectedItem():获得选中的元素
    getSelectedItem():获得选中的元素是列表中第几个元素
    slide TO() :解决鼠标移动到小圆点时跳转到哪一页
    slide Next():鼠标点击跳转到上一页
    slide Previous():鼠标点击跳转到下一页
    在这里插入图片描述
    具体实现:
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');
setInterval(() => {        /*定时轮播*/
  slider.slideNext()
}, 3000)
  1. 步骤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)

具体实现
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">
 //4个小圆点
    <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>

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

JS:

  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();
  • 优化1:插件/依赖注入
    避免插件和组件的强耦合,降低耦合度。
    比如:不想要小圆点的行为,可以直接把这个插件去掉,修改地方很少,但是还会显示小圆点,只不过失去了交互。
    具体实现
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();
  • 优化2:改进插件/模板化(可维护性大大提升)
    HTNL模板化,利于维护。
    在这里插入图片描述
    具体实现:
    标准的组件封装方式
    不需要小圆点的时候直接在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();
  • 优化3:组件模型抽象

UI组件框架的雏形
在这里插入图片描述
具体实现

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

三、局部细节控制

使用Function处理局部细节

  1. 逐渐消失的方块

HTML:

<div id="block" class="large">Click Me</div>
<p>文字内容文字内容文字内容文字内容文
  字内容文字内容文字内容文字内容文字内
  容文字内容文字内容文字内容文字内容文字
  内容文字内容文字内容文字内容文字内容文
  字内容文字内容文字内容文字内容文
  字内容文字内容文字内容文字内容文字内
  容文字内容文字内容文字内容文字内容文字
  内容文字内容文字内容文字内容文字内容文
  字内容文字内容文字内容文字内容文
  字内容文字内容文字内容文字内容文字内
  容文字内容文字内容文字内容文字内容文字
  内容文字内容文字内容文字内容文字内容</p>

CSS:

#block {
  float: left;
  color: white; 
  text-align: center;
  width: 150px;
  height: 150px;
  line-height: 150px;
  background-color: #37f;
  transition: opacity 2s; 
}

#block.hide{
  opacity: 0;
}

JS:

block.onclick = function(evt){
  console.log('hide');
  evt.target.className = 'hide';
  setTimeout(function(){
    document.body.removeChild(block);
  }, 2000);
};

运行效果:
在这里插入图片描述
点击后:
在这里插入图片描述

  1. 讨论:此处有 bug?
    不停的点击以后,报错:

在这里插入图片描述
应该限制响应函数被点击只能执行一次:

block.onclick = function(evt){
  console.log('hide');
  evt.target.className = 'hide';
  setTimeout(function(){
    document.body.removeChild(block);
  }, 2000);
};
  1. 异步请求获取数据
    HTML:
<script src="//lib.baomitu.com/axios/0.16.2/axios.js"></script>
<div>
<input id="t" name="t" value="hello" type="text"></input>
<input id="submitBtn" type="submit"></input>
</div>
<img id="gaobai" alt="未加载图片"></img>

JS:

const api = 'https://test.h5jun.com/index/gaobai?text=';

submitBtn.onclick = async function(evt){
  evt.preventDefault();
  
  let {data} = await axios.get(api + t.value);
  gaobai.src = 'data:image/jpeg;base64,' + data.data;
  console.log('data:image/jpeg;base64,' + data.data)
}
  1. 讨论:此处有 bug?
    点击应该只能执行一次:
const api = 'https://test.h5jun.com/index/gaobai?text=';

submitBtn.onclick = async function(evt){
  evt.preventDefault();
  
  let {data} = await axios.get(api + t.value);
  gaobai.src = 'data:image/jpeg;base64,' + data.data;
  console.log('data:image/jpeg;base64,' + data.data)
}
  1. 讨论:处理“只能执行一次”
    有很多“只允许执行一次”的函数操作,如何进行统一的抽象?
  • 过程抽象
    在这里插入图片描述
  • 抽象出一个函数:once
    只调用一次:
function once(fn){
  return function(...args){
    if(fn){
      let ret = fn.apply(this, args);
      fn = null;
      return ret;
    }
  }
}

function foo(idx){
  console.log(`I'm called:${idx}`);
}

foo(0);
foo(1);
foo(2);

foo = once(foo);

foo(3);
foo(4);
foo(5);

执行效果:
在这里插入图片描述

  • 点击一次
function once(fn){
  return function(...args){
    if(fn){
      let ret = fn.apply(this, args);
      fn = null;
      return ret;
    }
  }
}

block.onclick = once(function(evt){
  console.log('hide');
  evt.target.className = 'hide';
  setTimeout(function(){
    document.body.removeChild(block);
  }, 2000);
});
  • 提交一次
function once(fn){
  return function(...args){
    if(fn){
      let ret = fn.apply(this, args);
      fn = null;
      return ret;
    }
  }
}

const api = 'https://test.h5jun.com/index/gaobai?text=';

submitBtn.onclick = once(async function(evt){
  evt.preventDefault();
  
  let {data} = await axios.get(api + t.value);
  gaobai.src = 'data:image/jpeg;base64,' + data.data;
  console.log('data:image/jpeg;base64,' + data.data)
})
  • 节流:控制点击间隔时间
    HTML:
500毫秒可记录一次

<button id="btn">点我</button>

<div id="circle">0</div>

CSS:

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

JS:

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

随着鼠标移动的最终位置变动,小鸟飞行路径改变,鼠标停下来的时候,小鸟才会动。
HTML:

<script src="https://s1.ssl.qhres.com/!bd39e7fb/animator-0.2.0.min.js"></script>
<div id="bird" class="sprite bird1"></div>

CSS:

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

JS:

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

运行效果:
在这里插入图片描述

  • debounce:避免用户重复提交
    HTML:
<script src="//lib.baomitu.com/axios/0.16.2/axios.js"></script>
<div>
<input id="t" name="t" value="hello" type="text"></input>
<input id="submitBtn" type="submit"></input>
</div>
<img id="gaobai" alt="未加载图片"></img>

JS:

function debounce(fn){
  let timer = null
  return function(...args){
    if(timer != null) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.apply(this, args)
      timer = null
    }, 300)
  }
}

const api = 'https://test.h5jun.com/index/gaobai?text=';

submitBtn.onclick = debounce(async function(evt){
  evt.preventDefault();
  
  let {data} = await axios.get(api + t.value);
  gaobai.src = 'data:image/jpeg;base64,' + data.data;
  console.log('data:image/jpeg;base64,' + data.data)
})
  • 消费者
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(x, y){
  let sum = x + y;
  console.log(sum);
  return sum;
}

let consumerAdd = consumer(add, 1000);

let sum = 0;
for(let i = 0; i < 10; i++){
  consumerAdd(sum, i);
}

运行效果:
在这里插入图片描述

  • 连击
    HTML:
<div id="main">
  <button id="btn">Hit</button>
  <span id="count">+0</span>
</div>

CSS:

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

JS:

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)

运行效果:
在这里插入图片描述

  1. Declarative(声明式) v.s. Imperative(指令式)

Imperative(指令式):How to do?
怎么做

let list = [1, 2, 3, 4];

let map1 = [];
for(let i = 0; i < list.length; i++){
  map1.push(list[i] * 2);
}

Declarative(声明式):What to do?
不关心怎么做,关心做什么。

let list = [1, 2, 3, 4];

const double = x => x * 2;

list.map(double);

在这里插入图片描述

  • reduce方法
function add(x, y){
  return x + y;
}

function sub(x, y){
  return x - y;
}

console.log(add(add(add(1,2),3),4));  //不好!!
console.log([1, 2, 3, 4].reduce(add));
console.log([1, 2, 3, 4].reduce(sub));

运行结果:
在这里插入图片描述
-**Many方法

function add(x, y){
  return x + y;
}

function sub(x, y){
  return x - y;
}

function addMany(...args){
  return args.reduce(add);
}

function subMany(...args){
  return args.reduce(sub);
}

console.log(addMany(1,2,3,4));
console.log(subMany(1,2,3,4));

运行结果:
在这里插入图片描述

  • iterative方法
function iterative(fn){
  return function(...args){
    return args.reduce(fn.bind(this));
  }
}

const add = iterative((x, y) => x + y);
const sub = iterative((x, y) => x - y);

console.log(add(1,2,3,4));
console.log(sub(1,2,3,4));
  1. High-ordered functions(高阶函数)
    once、throttle、debounced、consumer、iterative

它们自身输入函数或返回函数,被称为高阶函数
在这里插入图片描述

  • toggle (imperative)
    HTML:
<div id="switcher" class="on"></div>

CSS:

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

JS:

switcher.onclick = function(evt){
  if(evt.target.className === 'on'){
    evt.target.className = 'off';
  }else{
    evt.target.className = 'on';
  }
}

在这里插入图片描述在这里插入图片描述

  • toggle (declarative)
    方便扩展,不需要改逻辑
    CSS:
#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;
}

JS:

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

三态
CSS:

#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.warn {
  background-color: yellow;
}

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

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

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

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

JS:

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 = 'warn',
  evt => evt.target.className = 'off',
  evt => evt.target.className = 'on'
);

在这里插入图片描述在这里插入图片描述在这里插入图片描述

  • 使用生成器
function * loop(list, max = Infinity){
  let i = 0;
  
  //noprotect
  while(i < max){
    yield list[i++ % list.length];
  }
}


function toggle(...actions){
  let action = loop(actions);
  return function(...args){
    return action.next().value.apply(this, args);
  }
}

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

四、总结

如何写好 JavaScript?

  1. 各司其职:JavaScript 尽量只做状态管理
  2. 结构、API、控制流分离设计 UI 组件
  3. 插件和模板化,并抽象出组件模型
  4. 运用过程抽象的技巧来抽象并优化局部 API
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值