这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战。
所谓"抽象化",就是指从具体问题中,提取出具有共性的模式,再使用通用的解决方法加以处理。
一个例子:交通灯切换
让你用原生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();
除了第一个版本不太行,其他版本都有自己的优缺点。抽象程度高,复用性高,但理解成本相应的也会很高,在很多时候我们需要做一个平衡。
我们追求的代码状态是:它既是一个优雅的代码,又不违背我们的直觉和思维习惯。
像本例中,我们只要把切换状态和等待两个函数切开来看,就很容易写出最后这个两全其美的代码。