360前端星计划0409

JavaScript从入门到放弃

1如何写好JS?

各司其职。即css就负责样式的部分,HTML就负责结构的部分,JS就负责行为的部分。不要在JS种进行一些修改样式的操作。

2 如何设计复杂UI组件(以一个轮播图为例)

2.1 结构设计

2.1.1 图片结构是一个列表型结构,所以主体用<ul>

2.1.2 使用CSS绝对定位将图片重叠在一个位置

2.1.3 轮播图切换的状态使用修饰符(modifier)

2.1.4 轮播图的切换动画使用css的transition

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

2.2 API设计

2.2.1设计原则

API设计的时候需要考虑执行什么样的原子操作,在轮播图种需要考虑以下几个方面:
getSelectedItem()获得选中的元素
getSelectedItemIndex()获取选中的元素是第几个元素,将轮播图下方小圆点与图片对应
slideTo()当前鼠标在这个小圆点时跳到哪个图片
slideNext()右边箭头点击的时候跳到下一页
slidePrevious()左边箭头点击的时候跳到上一页

2.2.2具体实现

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)

到这里就可以实现轮播图的基本流程

2.3控制流设置

2.3.1 控制结构

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

2.3.2 自定义事件

通过自定义事件实现事件的触发

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

2.3.3具体实现

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

到这里就可以实现轮播图的功能,但是还有些需要优化的地方。

2.4优化

上面的方式存在的问题是:整个轮播图是由图片、小圆点、左右箭头三部分构成的,这样做会产生的问题是:
1.构造函数太长
2.当需要删除小圆点或左右箭头时需要改动的部分太多
优化方法是:把他们做成插件的方式引入
具体实现

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

这样就可以把代码的耦合度降低
但是上面这样还存在一个问题就是想删除小圆点或者左右箭头还需要再把对应的HTML结构删除掉,所以需要进一步优化
优化2:改进插件/模板化

image.png

具体实现:

class Slider{
  constructor(id, opts = {images:[], cycle: 3000}){
    this.container = document.getElementById(id);
    this.options = opts;
//通过传递的图片对象生成一段HTML代码
    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();

HTML代码中就只需要一行代码即可

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

通过上面这样操作就可以只传递数据即可
优化3:组件模型抽象

image.png

具体实现:

//抽象出一个通用的组件类
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();

3.局部细节控制

3.1会出错的情况

3.1.1逐渐消失的方块

给一个方块绑定一个onclick事件,延时2s才开始执行。如果用户在2s内一直点击的话就会报错。只执行一次的解决方案如下

block.onclick = function(evt){
  block.onclick = null;
  console.log('hide');
  evt.target.className = 'hide';
  setTimeout(function(){
    document.body.removeChild(block);
  }, 2000);
};
//在新的浏览器中可以使用以下方法
/**
block.addEventListener('click',function(evt){
  block.onclick = null;
  console.log('hide');
  evt.target.className = 'hide';
  setTimeout(function(){
    document.body.removeChild(block);
  }, 2000);
},{once:true})
*/

3.1.2 异步请求获取数据

当用户点击提交长时间未响应的时候,用户可能会多次点击。如果是支付的场景下会造成用户多次付款。所以这里应该也限制用户只能执行一次

3.2 过程抽象

有很多“只允许执行一次”的函数操作,如何进行统一的抽象?

3.2.1 once函数

抽象成一个新的函数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函数多次执行
foo(0);
foo(1);
foo(2);
//这样不管foo调用几次就都只能执行一次
foo = once(foo);
foo(3);
foo(4);
foo(5);

3.2.2节流函数

使用场景:监听一个鼠标的移动或者滚轮的滚动执行相关操作的时候,如果不采用节流会导致多次触发,浪费资源
具体实现

function throttle(fn, time = 500){
  let timer;
  return function(...args){
    if(timer == null){
      fn.apply(this,  args);
      timer = setTimeout(() => {
        timer = null;
      }, time)
    }
  }
}
//使用节流后可以保证不管用户点击多块都每500毫秒记录一次
btn.onclick = throttle(function(e){
  circle.innerHTML = parseInt(circle.innerHTML) + 1;
  circle.className = 'fade';
  setTimeout(() => circle.className = '', 250);
});

3.2.3防抖函数

使用场景:监听用户的行为但是希望在用户停下来的时候再执行
具体实现

var i = 0;
setInterval(function(){
  bird.className = "sprite " + 'bird' + ((i++) % 3);
}, 1000/10);
//防抖
function debounce(fn, dur){
  dur = dur || 100;
  var timer;
//在用户执行过程中,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));

3.2.4消费者

使用场景:把一些同步的操作变成异步操作的方式。当用户触发了这个事件并不立即执行而是加到一个任务队列中,过一段时间再执行
具体实现

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

用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)
    }
  }
}

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)

用户一直点击,右侧数据记录点击次数,但显示的速度跟点击的速度并不一样,因为使用consumer将每次点击触发的事件添加到了队列中,按照规定好的时间去执行,执行的次数与点击的次数是一致的。

3.3 声明式编程(Declarative)与指令式编程(Imperative)

image.png

声明式编程关心的是做什么,它是一种逻辑的、函数式的例如:

let list = [1, 2, 3, 4];
//要让list里的每个数据变成原来的两倍,做什么?做double操作
const double = x => x * 2;

list.map(double);

指令式编程关心的是怎么做,它面向的是过程,对象例如:

let list = [1, 2, 3, 4];
//要让list里的每个数据变成原来的两倍,怎么做?用一个for循环来实现
let map1 = [];
for(let i = 0; i < list.length; i++){
  map1.push(list[i] * 2);
}

小例子

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

使用数组的reduce可以将数组的数据依次执行传进来的函数,但可以进一步优化

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

这样可以实现任意多个数据的操作,但还存在一个问题就是每新增一个功能就要重新定义一个**Many,所以还可以再抽象

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

使用上面这种方式,当有其他新增操作时就只需要写新增操作的逻辑就可以了。

3.4高阶函数

自身输入函数或返回函数的函数称为高阶函数

3.4.1小例子

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

这种写法存在的问题是不容易扩展,当有新的状态增加时需要重新编写相应的逻辑,可以改成下面这样,当有新的操作增加时只需要填写需要做什么即可。

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

上面的过程还可以进一步优化,使用生成器。

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

4总结

如何写好js?
1.各司其职:JavaScript 尽量只做状态管理
2.结构、API、控制流分离设计 UI 组件
3.插件和模板化,并抽象出组件模型
4.运用过程抽象的技巧来抽象并优化局部 API

Web标准:前端的原力

1.Web标准概述

Web标准是构成Web基础、运行和发展的一系列标准的总称。Web标准并不是由一家标准组织制定。

2.Web标准介绍

2.1.国际互联网工程任务组(The Internet Engineering Task Force,简称 IETF)

2.2.ecma

ECMA各版本的存档
https://www.ecma-international.org/publications/standards/Ecma-262-arch.htm

2.3.W3C

CSS

  1. CSS Containment Module Level 1
  2. Selectors Level 3
  3. CSS Fonts Module Level 3
  4. CSS Basic User Interface Module Level 3 (CSS3 UI)
  5. CSS Color Module Level 3
  6. CSS Namespaces Module Level 3
  7. CSS Style Attributes
  8. Selectors API Level 1
  9. Media Queries
  10. A MathML for CSS Profile
  11. Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification
  12. Associating Style Sheets with XML documents 1.0 (Second Edition)
  13. Document Object Model (DOM) Level 2 Style Specification

DOM

  1. Server-Sent Events
  2. Progress Events
  3. Element Traversal Specification
  4. Document Object Model (DOM) Level 3 Core Specification
  5. Document Object Model (DOM) Level 3 Load and Save Specification
  6. Document Object Model (DOM) Level 3 Validation Specification
  7. XML Events
  8. Document Object Model (DOM) Level 2 HTML Specification
  9. Document Object Model (DOM) Level 2 Style Specification
  10. Document Object Model (DOM) Level 2 Traversal and Range Specification
  11. Document Object Model (DOM) Level 2 Views Specification
  12. Document Object Model (DOM) Level 2 Core Specification
  13. Document Object Model (DOM) Level 2 Events Specification

Graphics

  1. Graphics Accessibility API Mappings
  2. WAI-ARIA Graphics Module
  3. HTML Canvas 2D Context
  4. WebCGM 2.1
  5. Scalable Vector Graphics (SVG) Tiny 1.2 Specification
  6. Portable Network Graphics (PNG) Specification (Second Edition)
  7. Mobile SVG Profiles: SVG Tiny and SVG Basic

HTML

  1. HTML Media Capture
  2. HTML 5.2
  3. HTML 5.1 2nd Edition
  4. Encrypted Media Extensions
  5. Media Source Extensions™
  6. Web Storage (Second Edition)
  7. HTML Canvas 2D Context
  8. XHTML+RDFa 1.1 - Third Edition
  9. RDFa Core 1.1 - Third Edition
  10. RDFa Lite 1.1 - Second Edition
  11. HTML+RDFa 1.1 - Second Edition
  12. HTML5 Image Description Extension (longdesc)
  13. CSS Style Attributes
  14. Internationalization Tag Set (ITS) Version 2.0
  15. Mobile Web Best Practices 1.0
  16. Document Object Model (DOM) Level 2 HTML Specification
  17. Ruby Annotation

HTTP

  1. Server-Sent Events

Performance

  1. Trace Context - Level 1
  2. WebAssembly Core Specification
  3. WebAssembly JavaScript Interface
  4. WebAssembly Web API
  5. High Resolution Time Level 2
  6. User Timing Level 2
  7. Performance Timeline
  8. Page Visibility (Second Edition)
  9. Navigation Timing

Security

  1. Web Authentication:An API for accessing Public Key Credentials Level 1
  2. Web Cryptography API
  3. Content Security Policy Level 2
  4. Subresource Integrity
  5. Cross-Origin Resource Sharing

Web API

  1. WebAssembly JavaScript Interface
  2. High Resolution Time Level 2
  3. Pointer Events
  4. User Timing Level 2
  5. WebDriver
  6. HTML Media Capture
  7. Indexed Database API 2.0
  8. Encrypted Media Extensions
  9. Web Cryptography API
  10. WebIDL Level 1
  11. Media Source Extensions™
  12. Geolocation API Specification 2nd Edition
  13. Pointer Lock
  14. Vibration API (Second Edition)
  15. Web Storage (Second Edition)
  16. Web Notifications
  17. HTML5 Web Messaging
  18. Server-Sent Events
  19. Indexed Database API
  20. Metadata API for Media Resources 1.0
  21. Progress Events
  22. Performance Timeline
  23. Page Visibility (Second Edition)
  24. Touch Events
  25. Selectors API Level 1
  26. Navigation Timing
  27. Element Traversal Specification

2.3.1BOM

BOM(Browser Object Model,浏览器对象模型)HTML5规范中有一部分涵盖了BOM的主要内容,因为W3C希望将JavaScript在浏览器中最基础的部分标准化。

1.window对象,也就是ECMAScript中定义的Global对象。网页中所有全局对象、变量和函数都暴露在这个对象上。
2.location对象,通过location对象可以以编程方式操纵浏览器的导航系统。
3.navigator对象,对象提供关于浏览器的信息。
4.screen对象,保存着客户端显示器的信息。
5.history对象,提供了操纵浏览器历史记录的能力。

2.3.2DOM

DOM(Document Object Model,文档对象模型)是HTML和XML文档的编程接口。DOM表示由多层节点构成的文档,通过它开发者可以添加、删除和修改页面的各个部分。DOM现在是真正跨平台、语言无关的表示和操作网页的方式。

DOM1(DOM Level 1)主要定义了HTML和XML文档的底层结构。DOM2(DOM Level 2)和DOM3(DOM Level 3)在这些结构之上加入更多交互能力,提供了更高级的XML特性。

DOM2和DOM3是按照模块化的思路来制定标准的,每个模块之间有一定关联,但分别针对某个DOM子集。

DOM Core:在DOM1核心部分的基础上,为节点增加方法和属性。

DOM Views:定义基于样式信息的不同视图。

DOM Events:定义通过事件实现DOM文档交互。

DOM Style:定义以编程方式访问和修改CSS样式的接口。

DOM Traversal and Range:新增遍历DOM文档及选择文档内容的接口。

DOM HTML:在DOM1 HTML部分的基础上,增加属性、方法和新接口。

DOM Mutation Observers:定义基于DOM变化触发回调的接口。这个模块是DOM4级模块,用于取代Mutation Events。

2.4 WHATWG

前端常用的HTTP知识

前端开发中的数据传输大多是基于 HTTP,了解掌握 HTTP 对于解决前端开发中遇到的问题是否有价值。

HTTP 是一个应用层协议,它的下层协议是 TCP/UDP。
报文格式
请求头

<method> <request-URL> <version>
<headers>
<entity-body>

响应头

<version> <status> <respon-phrase>
<headers>
<entity-body>

请求类型

请求类型 描述
GET 获取一个资源内容
POST 新增一个资源
PUT 更新资源内容
DELETE 删除资源
OPTIONS 根据返回判断是否有对其请求的权限(做一些跨域请求,看是否符合规范)
HEAD 只返回 head,不返回实体内容
PATCH 更新部分内容

状态码

状态码 描述
1xx 请求已接受,需要继续处理
2xx 请求已经正确处理
3xx 重定向
4xx 客户端错误
5xx 服务端错误

常见状态码

状态码 描述
101 切换协议,如:将 HTTP 协议切换为 WebSocket 协议
200 成功
206 返回部分内容,如:大文件下载
301 永久重定向,如:资源更换路径或改名
302 临时重定向,如:当前请求需要登录,临时跳转到登录页
304 资源未修改,不返回实体内容,客户端可直接读取本地缓存内容
400 错误请求
403 拒绝执行,如:无对应的访问权限
404 资源找不到,如:服务器已经删除该资源
413 请求实体过大,如:服务端限制了上传的文件大小
500 服务端内部错误,如:数据处理异常导致报错
502 作为网关或代理服务器时,上游服务器异常
504 作为网关或代理服务器时,上游服务器处理超时

URL
URL的写法是有一定规则的

<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag>
http://<host>:<port>/<path>?<query>#<frag>
ftp://<user>:<password>@<host>:<port>/<path>;<params>

Header分类
通用

Date: Tue, 3 Oct 2019 02:16:00 GMT
Connection: close
请求

User-Agent: Mozilla/5.0 (Linux; U; Android 4.0.2; en-us; Galaxy Nexus Build/ICL53F) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30
Accept: /
响应

Server: Nginx
Last-Modified: Thu, 16 Oct 2019 10:15:16 GMT

实体

Content-Type: text/html; charset=utf-8
Content-Length: 100

扩展(自定义)一般用X开头

X-Powered-By: thinkjs-3.0.4
X-Cache: hit
Cookie
HTTP 是无状态的,两次请求之间没有关联性。但实际业务中又有关联性的场景,cookie 可以用来解决这一问题。

请求头

在发送请求时带上cookie信息

GET / HTTP/1.1
Host: m.so.com
Connection: keep-alive
Cookie:__guid=34870781.3073803881376862000

响应头

HTTP/1.1 200 OK
Server: nginx/1.2.9
Date: Wed, 08 Oct 2014 05:59:59 GMT
Connection: keep-alive
Set-Cookie: thinkjs=s4mhqotbdbg9uh917lu8d5bub5; path=/
Content-Encoding: gzip

响应头里可以设置多个Set-Cookie,如下

Set-Cookie: <name>=<value>[; <name>=<value>]... [; expires=<date>][; domain=<domain_name>] [; path=<some_path>][; secure][; httponly][; samesite=<samesite_value>]

Cookie的安全策略
Cookie的属性相关

  • path
  • domain (hostonly*)
  • expires (max-age)
  • secure
  • httponly
  • SameSite
    只有当url与这些规则匹配的时候才可以携带cookie
    XSS漏洞盗取Cookie,可以设置httponly.这样就不可以通过document.cookie来获取cookie信息
    CSRF漏洞,通过设置token/samesite来防止
    Session
  • 服务器侧对应为 Session,基于 Cookie 存放用户信息
  • Cookie 有效期为 Session(随浏览器进程退出而失效)
    Content-Type
    返回的Content-Type:标识当前资源返回的内容是什么类型
    请求的Content-Type:标识提交数据的类型
类型
  • application/x-www-form-urlencoded
POST http://www.example.com HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8

title=test&sub%5B%5D=1&sub%5B%5D=2&sub%5B%5D=3
  • multipart/form-data
    一般用于上传一些二进制文件的时候
POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA

------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"

title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png

PNG ... content of chrome.png ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--
  • application/json
POST http://www.example.com HTTP/1.1 
Content-Type: application/json;charset=utf-8

{"title":"test","sub":[1,2,3]}
  • text/xml
POST http://www.example.com HTTP/1.1 
Content-Type: text/xml

<?xml version="1.0"?>
<methodCall>
    <methodName>examples.getStateName</methodName>
    <params>
        <param>
            <value><i4>41</i4></value>
        </param>
    </params>
</methodCall>

性能优化

  • keep-alive
    解决原先的时候发送完一次请求连接就关闭了的问题
    • HTTP 1.0 原本不支持 Keep-Alive,后来扩充了 Connection: Keep-Alive
      • HTTP 1.1 默认支持 Keep-Alive,除非显式指明 Connection: close
  • 减少网络传输大小
    在响应头中设置压缩方式content-encoding:gzip
    在请求头设置能够接收的压缩方式accept-encoding
    • 主要是文本资源(因为视频图片本身就有压缩,通过js再次压缩并不能减少多少,反而耗费性能)
    • 大约减少 60%
    • 文件过小不宜压缩(<1K)
  • 缓存
    • Last-Modified:通过比对这个值可以知道资源是否更新,如果没有则返回304直接从客户端选取文件即可


      Last-Modified比较规则.png
    • ETag:是一个编码值,具体怎么编码并没有规定,可以选择自己喜欢的方式


      ETag比较规则
    • Expires资源的最大有效期,在这个日期之前就使用缓存的文件,在这个日期之后就重新向服务器请求资源

指令 目的
Cache-Control:max-stale=<s> 缓存可以随意提供过期的文件。如果指定了<s>,在这段时间内,文档就不能过期。这使缓存规则放松。
Cache-Control:min-fresh=<s> 至少在未来<s>秒内文档要保持新鲜。这使缓存规则更加严格
Cache-Control:mac-age=<s> 缓存无法返回缓存时间超过<s>秒的文件。这使缓存规则更加严格。除非还同时发送了第一条指令。

Cache-Control:no-cache
Pragma:no-cache|除非资源进行了再验证,否则客户端不接受已缓存的资源
Cache-Control:no-store|缓存应尽快熊存储器中删除文档的所有痕迹,因为可能会包含敏感信息
Cache-Control:only-if-cached|只有当缓存中有副本存在时,客户端才会获取一份副本

  • Local Storage 可以存一些css 、js通过js本身来控制缓存
  • ServiceWorker
    拦截里面的一些请求,自己制定一些规则,来控制缓存情况
  • http2/http3
    协议本身的一些优化
    -HTTP2
    • 二进制传输
    • 多路复用
    • 头部压缩
    • server push(服务器推送)
  • HTTP3
  • 基于 QUIC 协议(UDP)
    HTTP抓包工具
  • Wireshark
  • Fiddler
  • Firebug for Firefox
  • Chrome 开发者工具
  • IE8+ 自带的开发者工具
    HTTP发包工具
  • telnet / curl
  • Fiddler *
  • Tamper for Firefox
  • Postman for Chrome *
  • Paw for OSX
    参考书籍
    《图解HTTP》
    《HTTP权威指南》
    《Web性能权威指南》
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容