【青训营】03 - 如何写好JavaScript - 过程抽象

184 阅读4分钟

好的代码是由工程师决定的,而非编程语言决定的。

技巧三 过程抽象

例子 ToDo List

cppaw-rj1yr.gif

HTML

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  <ul>
    <li><button></button><span>任务一:学习HTML</span></li>
    <li><button></button><span>任务二:学习CSS</span></li>
    <li><button></button><span>任务三:学习JavaScript</span></li>
  </ul>
</body>
</html>

CSS

ul {
  padding: 0;
  margin: 0;
  list-style: none;
}

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

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

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

li.completed button:before {
  content: '✅';
}

JavaScript

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

const list = document.querySelector('ul');
const buttons = list.querySelectorAll('button');
buttons.forEach((button) => { // 1
  button.addEventListener('click', once((evt) => {
    const target = evt.target;
    target.parentNode.className = 'completed';
    setTimeout(() => {
      list.removeChild(target.parentNode);
    }, 2000);
  }));
});

const foo = once(() => {
  console.log('bar');
});

foo();
foo();
foo();

这个需求是一个ToDo List,每点击完成一件事情,这个事件会在2秒后消失(1)。如果不加处理,用户在完成一件事后多次点击,就会出现列表同的一个buttonremoveChild()多次的Bug。如何实现一个button只被removeChild()一次呢?

我们当然可以直接在(1)中加上特判,来保证这一点。但仅执行一次的这个功能本身是可以通用化的,因此我们引入了once()装饰器(2),函数执行一次之后就把函数置为null,以此来保证函数仅执行一次。

为了能让只执行一次的需求覆盖不同的事件并处理,我们将这个需求剥离出来,这个过程称为过程抽象

这段代码中的函数装饰器once(),它以函数作为参数,以函数作为返回值,这样的函数我们称作高阶函数

常用高阶函数

节流

QQ截图20220118092106.png

在记录用户行为时,例如记录用户鼠标位置,如果不加以限制,就会将大量不必要的数据发往后台,造成带宽浪费。因此,我们可以进行节流设置,通过设置一个时间间隔,使得函数在同一段时间间隔内仅执行一次。

HTML

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  <ul>
    <li><button></button><span>任务一:学习HTML</span></li>
    <li><button></button><span>任务二:学习CSS</span></li>
    <li><button></button><span>任务三:学习JavaScript</span></li>
  </ul>
</body>
</html>

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

JavaScript

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

防抖

f5gbs-r8v58.gif

同样也是在记录用户行为时,如果我们让小鸟向用户鼠标所指的位置移动,而非随鼠标移动,此时就需要加入防抖进行限制。设置一个时间间隔,当鼠标静止够这个时间后,函数才执行。

HTML

<script src="https://s1.qhres2.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%;
}

JavaScript

var i = 0;
setInterval(function(){
  bird.className = "sprite " + 'bird' + ((i++) % 3);
}, 1000/10);

function debounce(fn, dur){ // 防抖
  dur = dur || 100;
  var timer;
  return function(){
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, dur);
  }
}

document.addEventListener('mousemove', debounce(function(evt){
  var x = evt.clientX,
      y = evt.clientY,
      x0 = bird.offsetLeft,
      y0 = bird.offsetTop;
  
  console.log(x, y);
  
  var a1 = new Animator(1000, function(ep){
    bird.style.top = y0 + ep * (y - y0) + 'px';
    bird.style.left = x0 + ep * (x - x0) + 'px';
  }, p => p * p);
  
  a1.animate();
}, 100));

Consumer

dokmv-l2ce9.gif

在于用户交互时,我们想等时间间隔的返回交互的结果,例如快速点击Hit,但连击数字显示会等时间间隔进行累加。

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

JavaScript

function consumer(fn, time){ 
  let tasks = [],
      timer;
  
  return function(...args){
    tasks.push(fn.bind(this, ...args));
    if(timer == null){
      timer = setInterval(() => {
        tasks.shift().call(this)
        if(tasks.length <= 0){
          clearInterval(timer);
          timer = null;
        }
      }, time)
    }
  }
}

btn.onclick = consumer((evt)=>{
  let t = parseInt(count.innerHTML.slice(1)) + 1;
  count.innerHTML = `+${t}`;
  count.className = 'hit';
  let r = t * 7 % 256,
      g = t * 17 % 128,
      b = t * 31 % 128;
  
  count.style.color = `rgb(${r},${g},${b})`.trim();
  setTimeout(()=>{
    count.className = 'hide';
  }, 500);
}, 800)

Iterative

szgvg-o41kj.gif

将不可迭代的方法批量进行操作。

HTML

<ul>
  <li>a</li>
  <li>b</li>
  <li>c</li>
  <li>d</li>
  <li>e</li>
  <li>f</li>
  <li>g</li>
</ul>

CSS

JavaScript

const isIterable = obj => obj != null 
  && typeof obj[Symbol.iterator] === 'function';

function iterative(fn) {
  return function(subject, ...rest) {
    if(isIterable(subject)) {
      const ret = [];
      for(let obj of subject) {
        ret.push(fn.apply(this, [obj, ...rest]));
      }
      return ret;
    }
    return fn.apply(this, [subject, ...rest]);
  }
}

const setColor = iterative((el, color) => {
  el.style.color = color;
});

const els = document.querySelectorAll('li:nth-child(2n+1)');
setColor(els, 'red');

为什么要使用高阶函数?

在实际项目系统中,函数分为两种:

  • 纯函数:无状态,结果唯一确定;
  • 非纯函数:有状态,调用次序/时间不同,得到的结果就不同; 纯函数本身结果可控,且更易于测试。因此我们应在实践中,利用高阶函数(纯函数)减少非纯函数的使用,从而增加系统的稳定性和可靠性。

参考资料