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

主讲人:月影

思考:如何写“好” JavaScript

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

一、各司其职

JavaScript :行为
CSS :表现
HTML:结构
尽量做到职责分离
第六届360前端星计划_JavaScript 从入门到放弃_第1张图片

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

运行效果:

第六届360前端星计划_JavaScript 从入门到放弃_第2张图片
点击红色按钮后:
第六届360前端星计划_JavaScript 从入门到放弃_第3张图片

  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():鼠标点击跳转到下一页
    第六届360前端星计划_JavaScript 从入门到放弃_第4张图片
    具体实现:
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模板化,利于维护。
    第六届360前端星计划_JavaScript 从入门到放弃_第5张图片
    具体实现:
    标准的组件封装方式
    不需要小圆点的时候直接在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 `
    ${content.join('')}
`
; } 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) => ` ${i===0?'--selected':''}"> `).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 ``; }, 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 ``; }, 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组件框架的雏形
第六届360前端星计划_JavaScript 从入门到放弃_第6张图片
具体实现

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 => `
      
  • ${image}"/>
  • `
    .trim()); return `
      ${content.join('')}
    `
    ; } 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) => ` ${i===0?'--selected':''}"> `).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 ``; }, 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 ``; }, 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);
    };
    

    运行效果:
    第六届360前端星计划_JavaScript 从入门到放弃_第7张图片
    点击后:
    第六届360前端星计划_JavaScript 从入门到放弃_第8张图片

    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. 讨论:处理“只能执行一次”
      有很多“只允许执行一次”的函数操作,如何进行统一的抽象?
    • 过程抽象
      第六届360前端星计划_JavaScript 从入门到放弃_第9张图片
    • 抽象出一个函数: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);
    

    执行效果:
    第六届360前端星计划_JavaScript 从入门到放弃_第10张图片

    • 点击一次
    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);
    });
    

    运行效果:
    第六届360前端星计划_JavaScript 从入门到放弃_第11张图片

    • 呆萌的小鸟: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));
    

    运行效果:
    第六届360前端星计划_JavaScript 从入门到放弃_第12张图片

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

    运行效果:
    第六届360前端星计划_JavaScript 从入门到放弃_第13张图片

    • 连击
      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)
    
    

    运行效果:
    第六届360前端星计划_JavaScript 从入门到放弃_第14张图片

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

    第六届360前端星计划_JavaScript 从入门到放弃_第15张图片

    • 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

    它们自身输入函数或返回函数,被称为高阶函数
    第六届360前端星计划_JavaScript 从入门到放弃_第16张图片

    • 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

    你可能感兴趣的:(第六届360前端星计划)