# 三。过程抽象
处理局部细节控制的一些方法
函数式编程思想的基础应用
# 应用:操作次数限制
- 一些异步交互
- 一次性的 HTTP 请求
有这样一段代码,在每次点击时延时 2s 后移除该节点,但如果用户在该节点还没完全移除的时候又点了几次则会报错。
const list = document.querySelector('ul'); | |
const buttons = list.querySelectorAll('button'); | |
buttons.forEach((button)=>{ | |
button.addEventListener('click', (evt) => { | |
const target = evt.target; | |
target.parentNode.className = 'completed'; | |
setTimeout(()=>{ | |
list.removeChild(target.parentNode); | |
},2000); | |
}) | |
}); |
而这个操作次数的限制,则可以抽象出来一个高级函数
function once(fn) { | |
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)=>{ | |
button.addEventListener('click', once((evt) => { | |
const target = evt.target; | |
target.parentNode.className = 'completed'; | |
setTimeout(()=>{ | |
list.removeChild(target.parentNode); | |
},2000); | |
})) | |
}); |
如代码中显示的那样,这个函数 once 接受一个函数,返回的也是一个函数,判断接受的函数是否为 null,若不为 null 则执行这个函数并返回其结果,若接受的函数为 null 则返回一个不进行任何操作的函数。click 事件注册的实际上是 once 返回的函数,这样再怎么点击也不会报错了。
ps:好精彩的应用例子!
为了让 ” 只执行一次 “ 这个需求覆盖不同的事件处理,将这个需求剥离出来,这个过程就称之为 过程抽象
# 高阶函数
- 以函数作为参数
- 以函数作为返回值
- 常用于作为 函数装饰器
funtion HOF(fn) { | |
return function(...args) { | |
return fn.apply(this, args); | |
} | |
} |
# 常用高阶函数
# Once 只执行一次
前文讲过,这里不再阐述
# Throttle 节流
为函数添加一个间隔 time,每隔 time 事件调用一次函数,节省其需求,比如某个事件很容易持续的发生(如鼠标移上去就触发),那么他会一直速度特别快的调用这个事件函数,这个时候为其加一个节流函数则可以防止崩溃节约流量。
function throttle(fn, time = 500) { | |
let timer; | |
return function(...args) { | |
if(timer == null) { | |
fn.apply(this, args); | |
timer = setTimeout(() => { | |
timer = null; | |
}, timer); | |
} | |
} | |
} | |
btn.onclick = throttle(function(e){ | |
/* 事件处理 */ | |
circle.innerHTML = parseInt(circle.innerHTML)+1; | |
circle.className = 'fade'; | |
setTimeout(() => circle.className = '', 250); | |
}); |
对原始的函数进行包装,没有 timer 的话就注册一个 timer,500ms 后取消,因为在这 500ms 中这个 timer 都还存在,所以不会去执行函数(或者说执行空函数),500ms 后 timer 取消了,函数就可以被调用执行了。
# Debounce 防抖
在上面的节流中,timer 存在期间是不会去执行函数,而防抖是在每次事件一开始的时候清空 timer,然后设置 timer 为 dur,当事件调用 dur 时间并且没有新的事件再次调用时(比如鼠标移动后悬停一段时间),函数就可以被调用执行了。
function debounce(fn, dur) { | |
dur = dur || 100; //dur 若不存在则设置 dur 为 100ms | |
var timer; | |
return function() { | |
clearTimeout(timer); | |
timer = setTimeout(() => { | |
fn.apply(this, arguments); | |
}, dur); | |
} | |
} |
# Consumer
这是将一个函数变成类似 setTimeout 这样的异步操作的函数,如调用了很多次某事件,将这些事件丢到一个列表中,按设定好的时间隔一段时间并执行返回其结果。先来看代码:
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.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); |
这里的事件处理实现了点击按钮时执行这个不断显示 + count 并在 500ms 后渐隐,而快速点击时,则将这个点击事件存储到是事件列表中每隔 800ms 执行(不然上一个 + count 还未消失)。
要弄明白函数原理,得从其中的 bind 函数和 shift 函数和 call 说起:
bind()
方法创建一个新的函数,在bind()
被调用时,这个新函数的this
被指定为bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
shift()
方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。与之相反的则是unshift()
插入第一个元素。与之相似的一对方法还有,
pop()
和push()
,他们作用于数组最后一个元素
call()
方法使用一个指定的this
值和单独给出的一个或多个参数来调用一个函数。
那么不难看出上面这个函数的用途,将每次准备调用的函数放入 tasks 列表中,若定时器为空则设置一个定时器执行内容 定时执行tasks出队,若全部tasks已经清空(当前没有任务了)则将定时器清除
,若定时器不为空则不做操作(但放到 tasks 列表中了)。
# Iterative
将一个函数,变成可迭代使用的的,这通常用于一个函数要给一组对象执行批量操作的时候。如批量设置颜色,代码如下:
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'); |
# Toggle
切换状态,也可以封装成一个高级函数,这样有多少种状态只要添加到里面就可以了。
例子:
function toggle(...actions) { | |
return function (...args) { | |
let action = actions.shift(); | |
action.push(action); | |
return action.apply(this, args); | |
} | |
} | |
// 多少态都可以! | |
switcher.onclick = toggle( | |
evt => evt.target.className = 'off', | |
evt => evt.target.className = 'on' | |
); |