博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
360前端星学习笔记-如何写‘好’JavaScript
阅读量:5791 次
发布时间:2019-06-18

本文共 18259 字,大约阅读时间需要 60 分钟。

前言

《如何写‘好’javascript》这门课是由360技术专家月影老师讲的。

说实话,我一直在纠结要不要写关于js的文章,因为对于js来说,我的实际经验不足,更不要说面向对象编程与函数式编程了,对于过程抽象与行为抽象也没有深入的理解,但想想还是觉得应该分享出来,并且我尽量原汁原味的阐述这门课的内容,尽量不加入自己主观理解,因为对于没有实际经验的我来说,如果添加自己主观的理解只能误导读者,好了,不费话了~

一、关灯吃面

gif

需求:

  • 点击红色按钮
  • 背景变成黑色
  • 字体color由黑色变成白色
  • 红色按钮变成绿色

1.1 版本1

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

对于我来说,要是让我完成这个需求,大概应该就写成这样吧^_^,

想想这样写好不好呢?

答案肯定是不好的。

这样写的问题:

  • 用js直接去修改了元素的样式。
  • 并且代码只能看出修改了一些元素的样式,看不出这坨代码需要完成哪些需求。
  • 假设:如果以后想改需求了,比如开灯时字体变为红色,或者需要添加一些功能,那我就得去重新看代码,去改这一坨代码,这样的话,维护起来就非常难。

1.2 版本2:

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

这回代码语义化就比较强了,通过js去修改className而不是用js来直接修改style,这样写会比较好一点。

1.3 版本3:其他思路

    今天回到家,    煮了点面吃,    一边吃面一边哭,    泪水滴落在碗里,    没有开灯。    

这么写的思路就是不使用js,而是通过input和label关联来切换状态。

二、复杂的UI组件的设计

轮播图

这是大家最熟悉不过的轮播图组件了,如果用面向过程的写法,可能会出现很多bug,那么如何实现才是最好的呢?

2.1 步骤1:整体思路

整体思路

  1. 图片结构是一个列表型结构,所以主体用 <ul><li>
  2. 使用 css 绝对定位将图片重叠在同一个位置
  3. 轮播图切换的状态使用修饰符(modifier)
  4. 轮播图的切换动画使用 css transition

2.2 步骤2: API设计

img

具体实现:

class Slider{  constructor(id){    this.container = document.getElementById(id);    this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');  }  // 获得当前元素  getSelectedItem(){    const selected = this.container.querySelector('.slider-list__item--selected');    return selected  }  // 获得当前元素的索引  getSelectedItemIndex(){    return Array.from(this.items).indexOf(this.getSelectedItem());  }  // 切换到第index张图片  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);    }}// 通过new来实例化const slider = new Slider('my-slider');setInterval(() => {  slider.slideNext()}, 3000)

2.3 步骤3:控制流设计 (下方小圆点与左右按钮设计)

控制结构

自定义事件

const detail = {index: idx}    const event = new CustomEvent('slide', {bubbles:true, detail})    this.container.dispatchEvent(event)

因为下方原点与图片自动切换的下标(index)是一致的,所以可以通过事件机制,在图片slide时候直接给container派发一个事件,这样的话呢,通过container去监听这个事件,去更新控制结构上小圆点的状态。

具体实现:

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();      });      // 监听slide事件      this.container.addEventListener('slide', evt => {        // 拿到slide事件传来的index        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();

这个实现的构造函数会复杂一些,但是把timer定时器也封装进去了,会有轮播的时间默认为3秒钟,同样的也是获得container,items,cycle(时间)通过事件机制将控制流中的小圆点与图片联动起来。并且还判断了controler是否存在,假如以后我们不需要小圆点这个功能了,我们只需要把html中相关的结构去掉,js也不会报错,但是这里还有一个优化的点就是slider与controler之间有着比较强的耦合度。

2.4 控制流设计原则

为什么要用到事件机制呢?因为要降低结构之间的耦合度,如果不这样做的话,我们需要做双向的操控的。

举个栗子?

比如我们要添加一个需求:显示当前index。

只需要这样做:

  1. 结构中添加
第0张
  1. js中添加
document.addEventListener('slider', (evt) => {    other.innerHTML = `第${evt.detail.index}张`})

三、这样是不是就可以交差了呢?

其实还是有很大的改动空间的,比如上面的代码在构造函数的代码量特别多,slider与controler的耦合度比较大,如何降低它们之间的耦合度呢?

img

3.1 优化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();

这样做的好处:比如我们不想要controler这个组件了,直接删掉插件与html对应结构,其他的功能还是可以正常使用。

3.2 优化2:改进插件/模板化

上面的代码还不是特别的优雅,当我们不想要一个功能时,需要删除html结构与js代码,如果用模板化,只需要修改js即可。

img

render方法会传data数据,负责构造html结构

action方法会注入component对象,负责初始化这个对象,添加事件、行为。

这样我们的html结构只有

class Slider{  constructor(id, opts = {images:[], cycle: 3000}){    this.container = document.getElementById(id);    this.options = opts;    this.container.innerHTML = this.render();    this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');    this.cycle = opts.cycle || 3000;    this.slideTo(0);  }  render(){    const images = this.options.images;    const content = images.map(image => `      
  • `.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 `
    ${images.map((image, i) => `
    `).join('')}
    `.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();

    这样做的好处就是我们可以随意修改这个组件的功能,如果不想要两边的按钮或者控制流的小圆点,只需要修改注册插件即可。

    插件化/模板化这种做法还有一个缺点就是如果我们修改插件时,我们直接append到组件里,可能只修改了一点点代码,最后导致整个dom都刷新了,这就是为什么现在一些主流框架采用虚拟dom的方式,通过diff算法来局部修改dom。

    3.3 优化3:组件模型抽象

    img

    最终实现:

    class Component{  constructor(id, opts = {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 = '.slider-list__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 = {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 => `      
  • `.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 `
    ${images.map((image, i) => `
    `).join('')}
    `.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', {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();

    四、谈一谈‘抽象’

    我们从最初的需求开始一步一步的得到最终组件抽象的这个模型,我们理清楚了组件和插件的关系,以及他们之间应该怎样完成渲染,这里面很重要的是我们一步步的在做抽象,一步步的抽象出来这些元素,然后一步步的拆解这些元素之间的依赖关系,尽量把他们独立出来,不管组件也好还是插件也好,我们都希望未来当我们这个ui、交互有一小部分的变化的时候,我们只要去修改、重建这部分变化所涉及到的插件或者组件就可以了,而不用动整个这个代码结构,这样让我们代码的健壮性和可维护性就大大的增强了,我们就可以把这个组件发布出来了。

    4.1 回顾一下

    1. 组件的结构
    2. 组件的api设计
    3. 控制流的设计

    这三个东西设计完之后,通过一些技巧,把这个组件这三个部分给封装好,并且把他们抽象出来,降低他们的耦合度。比如我们用到了依赖注入技巧、自定义事件技巧、模板化的技巧,这些技巧都可以让我们设计出低耦合度的ui组件。

    五、最后

    我一直觉得一篇文章过多的代码会让读者感到视觉疲劳,但实在是没有需要修改的地方,非常建议大家一步步的敲一遍,深刻体会月影大大写的javascript是多么的优雅?~~

    转载地址:http://dqwfx.baihongyu.com/

    你可能感兴趣的文章
    HTML基础(一)
    查看>>
    boost.circular_buffer简介
    查看>>
    Database Appliance并非Mini版的Exadata-还原真实的Oracle Unbreakable Database Appliance
    查看>>
    网页图片缩放(js)
    查看>>
    Perl开发的几个小注意事项
    查看>>
    实现hive proxy1-hive认证实现
    查看>>
    设计和使用维护计划
    查看>>
    Shiro 基础教程
    查看>>
    div的作用
    查看>>
    Identifier 'Logic.DomainObjectBase._isNew' is not CLS-compliant
    查看>>
    php操作oracle的方法类集全
    查看>>
    03数据结构——线性表(头节点和头指针)
    查看>>
    JavaScript-函数
    查看>>
    Response的使用入门
    查看>>
    mysql----------mysql的一些常用命令
    查看>>
    推荐系统漫谈
    查看>>
    Netty之有效规避内存泄漏
    查看>>
    新建Maven项目出错
    查看>>
    UVa 10714 - Ants
    查看>>
    目录和文件管理
    查看>>