【青训营】写好JS——做好抽象

168 阅读3分钟

这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战

所谓"抽象化",就是指从具体问题中,提取出具有共性的模式,再使用通用的解决方法加以处理。

一个例子:交通灯切换

让你用原生JS实现一个交通灯切换的组件,怎么抽象?怎么提高扩展性和复用性?

GIF

新手入门

你可能会觉得红绿灯很简单,三个状态用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();

除了第一个版本不太行,其他版本都有自己的优缺点。抽象程度高,复用性高,但理解成本相应的也会很高,在很多时候我们需要做一个平衡。

我们追求的代码状态是:它既是一个优雅的代码,又不违背我们的直觉和思维习惯。

像本例中,我们只要把切换状态和等待两个函数切开来看,就很容易写出最后这个两全其美的代码。