好的代码是由工程师决定的,而非编程语言决定的。
技巧三 过程抽象
例子 ToDo List
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)。如果不加处理,用户在完成一件事后多次点击,就会出现列表同的一个button被removeChild()多次的Bug。如何实现一个button只被removeChild()一次呢?
我们当然可以直接在(1)中加上特判,来保证这一点。但仅执行一次的这个功能本身是可以通用化的,因此我们引入了once()装饰器(2),函数执行一次之后就把函数置为null,以此来保证函数仅执行一次。
为了能让只执行一次的需求覆盖不同的事件并处理,我们将这个需求剥离出来,这个过程称为过程抽象。
这段代码中的函数装饰器once(),它以函数作为参数,以函数作为返回值,这样的函数我们称作高阶函数。
常用高阶函数
节流
在记录用户行为时,例如记录用户鼠标位置,如果不加以限制,就会将大量不必要的数据发往后台,造成带宽浪费。因此,我们可以进行节流设置,通过设置一个时间间隔,使得函数在同一段时间间隔内仅执行一次。
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);
});
防抖
同样也是在记录用户行为时,如果我们让小鸟向用户鼠标所指的位置移动,而非随鼠标移动,此时就需要加入防抖进行限制。设置一个时间间隔,当鼠标静止够这个时间后,函数才执行。
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
在于用户交互时,我们想等时间间隔的返回交互的结果,例如快速点击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
将不可迭代的方法批量进行操作。
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');
为什么要使用高阶函数?
在实际项目系统中,函数分为两种:
- 纯函数:无状态,结果唯一确定;
- 非纯函数:有状态,调用次序/时间不同,得到的结果就不同; 纯函数本身结果可控,且更易于测试。因此我们应在实践中,利用高阶函数(纯函数)减少非纯函数的使用,从而增加系统的稳定性和可靠性。