所谓"抽象化",就是指从具体问题中,提取出具有共性的模式,再使用通用的解决方法加以处理。
一个例子:交通灯切换
让你用原生JS实现一个交通灯切换的组件,怎么抽象?怎么提高扩展性和复用性?
新手入门
你可能会觉得红绿灯很简单,三个状态用setTimeout()
嵌套一下就可以了:
const traffic = document.getElementById('traffic'); (function reset() { traffic.className = 's1'; setTimeout(function () { traffic.className = 's2'; setTimeout(function () { traffic.className = 's3'; setTimeout(reset, 1000) }, 1000) }, 1000); })(); 复制代码
但是,这时候如果需要复用,再加两个灯怎么办?再嵌套两个状态?
(function reset() { traffic.className = 's1'; setTimeout(function () { traffic.className = 's2'; setTimeout(function () { traffic.className = 's3'; setTimeout(function () { traffic.className = 's4'; setTimeout(function () { traffic.className = 's5'; setTimeout(reset, 1000) }, 1000) }, 1000) }, 1000) }, 1000); })(); 复制代码
这样显然不行,多个异步函数回调会造成“callback hell”,有同学可能会说那用setInterval()
就可以了,但是如果每个灯的持续时间不同呢?
数据抽象
第二个版本我们对交通灯做一个数据的抽象封装:
const traffic = document.getElementById('traffic'); const stateList = [ { state: 'wait', last: 1000 }, { state: 'stop', last: 3000 }, { state: 'pass', last: 3000 }, ]; function start(traffic, stateList) { function applyState(stateIdx) { const { state, last } = stateList[stateIdx]; traffic.className = state; setTimeout(() => { applyState((stateIdx + 1) % stateList.length); }, last) } applyState(0); } start(traffic, stateList); 复制代码
通过传参调用setTimeout()
来创建新的状态和持续时间,这样我们就得到了一个通用的版本。如果需要新的状态就在stateList
里添加即可:
const stateList = [ { state: 'wait', last: 1000 }, { state: 'stop', last: 3000 }, { state: 'pass', last: 3000 }, { state: 'new', last: 500} ]; 复制代码
不过因为我们没有做模板的抽象,所以要手动地添加部分HTML和CSS代码。
过程抽象
第三个版本我们使用刚讲过的过程抽象将其抽象成一个轮询的函数,每次去执行异步函数就可以了:
const traffic = document.getElementById('traffic'); function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function poll(...fnList) { let stateIndex = 0; return async function (...args) { let fn = fnList[stateIndex++ % fnList.length]; return await fn.apply(this, args); } } async function setState(state, ms) { traffic.className = state; await wait(ms); } let trafficStatePoll = poll( setState.bind(null, 'wait', 1000), setState.bind(null, 'stop', 3000), setState.bind(null, 'pass', 3000) ); (async function () { // noprotect while (1) { await trafficStatePoll(); } }()); 复制代码
简化通用
过度的抽象是一种负担。
wait()
是异步的,setState()
是瞬间的,这样既简化了代码,也更符合人的直觉:
const traffic = document.getElementById('traffic'); function wait(time) { return new Promise(resolve => setTimeout(resolve, time)); } function setState(state) { traffic.className = state; } async function start() { //noprotect while (1) { setState('wait'); await wait(1000); setState('stop'); await wait(3000); setState('pass'); await wait(3000); } } start(); 复制代码
除了第一个版本不太行,其他版本都有自己的优缺点。抽象程度高,复用性高,但理解成本相应的也会很高,在很多时候我们需要做一个平衡。
我们追求的代码状态是:它既是一个优雅的代码,又不违背我们的直觉和思维习惯。
像本例中,我们只要把切换状态和等待两个函数切开来看,就很容易写出最后这个两全其美的代码。