如何寫出好的 JavaScript —— 淺談 API 設計

rjmorusejm 7年前發布 | 8K 次閱讀 API JavaScript開發 JavaScript

很多同學覺得寫 JavaScript 很簡單,只要能寫出功能來,效果能實現就好。還有一些培訓機構,專門教人寫各種“炫酷特效”,以此讓許多人覺得這些培訓很“牛逼”。然而事實上,能寫 JavaScript 和寫好 JavaScript 這中間還有很遙遠的距離。成為專業前端,注定在 JavaScript 路途上需要一步步扎實的修煉,沒有捷徑。

看一個簡單的例子:

實現一個類似于“交通燈”的效果,讓三個不同顏色的圓點每隔 2 秒循環切換。

對應的 HTML 和 CSS 如下:

<ulid="traffic" class="wait">
  <li><span></span></li>
  <li><span></span></li>
  <li><span></span></li>
</ul>
#traffic > li{
  display: block;
}
 
#traffic span{
  display: inline-block;
  width: 50px;
  height: 50px;
  background-color: gray;
  margin: 5px;
  border-radius: 50%;
}
 
#traffic.stop li:nth-child(1) span{
  background-color: #a00;
}
 
#traffic.wait li:nth-child(2) span{
  background-color: #aa0;
}
 
#traffic.pass li:nth-child(3) span{
  background-color: #0a0;
}

那么這一功能的 JavaScript 該如何實現呢?

版本一

有的同學說,這個實現還不簡單嘛?直接用幾個定時器一下切換不就好了:

const traffic = document.getElementById("traffic");
 
(function reset(){
  traffic.className = "wait";
 
  setTimeout(function(){
      traffic.className = "stop";
      setTimeout(function(){
        traffic.className = "pass";
        setTimeout(reset, 2000)
      }, 2000)
  }, 2000);
})();

沒錯,就這個功能本身,這樣實現就 OK 了。但是這樣實現有什么問題呢?

首先是 過程耦合 ,狀態切換是wait->stop->pass 循環,在上面的設計里,實際上操作順序是耦合在一起的,要先 ‘wait’,然后等待 2000 毫秒再 ‘stop’,然后再等待 2000 毫秒在 ‘pass’,這中間的順序一旦有調整,需求有變化,代碼都需要修改。

其次,這樣的異步嵌套是會產生 callback hell 的,如果需求不是三盞燈,而是五盞燈、十盞燈,代碼的嵌套結構就很深,看起來就很難看了。

所以我們說,版本一方法雖然直接,但因為抽象程度很低(幾乎沒有提供任何抽象 API),它的擴展性很不好,因為異步問題沒處理,代碼結構也很不好。如果只能寫這樣的代碼,是不能說就寫好了 JavaScript 的。

版本二

要解決版本一的 過程耦合 問題,最簡單的思路是將狀態 ['wait','stop','pass'] 抽象出來:

const traffic = document.getElementById("traffic");
 
var stateList = ["wait", "stop", "pass"];
 
var currentStateIndex = 0;
 
setInterval(function(){
  var state = stateList[currentStateIndex];
  traffic.className = state;
  currentStateIndex = (currentStateIndex + 1) % stateList.length;
}, 2000);

這是一種數據抽象的思路,應用它我們得到了上面的這個版本。

這一版本比前一版本要好很多,但是它也有問題,最大的問題就是 封裝性很差 ,它把 stateList 和 currentStateIndex 都暴露出來了,而且以全局變量的形式,這么做很不好,需要優化。

版本三

const traffic = document.getElementById("traffic");
 
function start(traffic, stateList){
  var currentStateIndex = 0;
 
  setInterval(function(){
    var state = stateList[currentStateIndex];
    traffic.className = state;
    currentStateIndex = (currentStateIndex + 1) % stateList.length;
  }, 2000);
}
 
start(traffic, ["wait", "stop", "pass"]);

版本三是中規中矩的一版,也是一般我們在工作中比較常用的思路。應該將暴露出來的 API 暴露出來(本例中的 stateList)。將不應該暴露出來的數據或狀態隱藏(本例中的 currentStateIndex)。

有許多同學覺得說寫出這一版本來已經很不錯的。的確,應該也還不錯,但這一版的抽象程度其實也不是很高,或者說,如果考慮適用性,這版已經很好了,但是如果考慮可復用性的話,這版依然有改進空間。

我們再看一個思路上較有意思的版本。

版本四

const traffic = document.getElementById("traffic");
 
function poll(...fnList){
  letstateIndex = 0;
 
  return function(...args){
    letfn = fnList[stateIndex++ % fnList.length];
    return fn.apply(this, args);
  }
}
 
function setState(state){
  traffic.className = state;
}
 
lettrafficStatePoll = poll(setState.bind(null, "wait"),
                            setState.bind(null, "stop"),
                            setState.bind(null, "pass"));
 
setInterval(trafficStatePoll, 2000);

這一版用的是 過程抽象 的思路,而過程抽象,是 函數式編程 的基礎。在這里,我們抽象出了一個 poll(...fnList) 的高階組合函數,它將一個函數列表組合起來,每次調用時依次輪流執行列表里的函數。

我們說,程序設計的本質是抽象,而 過程抽象 是一種與 數據抽象 對應的思路,它們是兩種不同的抽象模型。數據抽象比較基礎,而過程抽象相對高級一些,也更靈活一些。數據抽象是研究函數如何操作數據,而過程抽象則在此基礎上研究函數如何操作函數。所以說如果把抽象比作數學,那么數據抽象是初等數學,過程抽象則是高等數學。同一個問題,既可以用初等數學來解決,又可以用高等數學來解決。用什么方法解決,取決于問題的模型和難度等等。

好了,上面我們有了四個版本,那么是否考慮了這些版本就足夠了呢?

并不是。因為需求是會變更的。假設現在需求變化了:

需求變更:讓 wait、stop、pass 狀態的持續時長不相等,分別改成 1秒、2秒、3秒。

那么,我們發現 ——

除了版本一之外,版本二、三、四全都跪了……

那是否意味著我們要 回歸到版本一 呢?

當然并不是。

版本五

const traffic = document.getElementById("traffic");
 
function wait(time){
  return new Promise(resolve => setTimeout(resolve, time));
}
 
function setState(state){
  traffic.className = state;
}
 
function reset(){
  Promise.resolve()
    .then(setState.bind(null, "wait"))
    .then(wait.bind(null, 1000))
    .then(setState.bind(null, "stop"))
    .then(wait.bind(null, 2000))
    .then(setState.bind(null, "pass"))
    .then(wait.bind(null, 3000))
    .then(reset);
}
 
reset();

版本五的思路是, 既然我們需要考慮不同的持續時間,那么我們需要將等待時間抽象出來 :

function wait(time){
  return new Promise(resolve => setTimeout(resolve, time));
}

這一版本里我們用了 Promise 來處理回調問題,當然對 ES6 之前的版本,可以用 shim 或 polyfill、第三方庫,也可以選擇不用 Promise。

版本五抽象出的 wait 方法也還比較通用,可以用在其他地方。這是版本五好的一點。

版本六

我們還可以進一步抽象,設計出版本六,或者類似的 對象模型

const trafficEl = document.getElementById("traffic");
 
function TrafficProtocol(el, reset){
  this.subject = el;
  this.autoReset = reset;
  this.stateList = [];
}
 
TrafficProtocol.prototype.putState = function(fn){
  this.stateList.push(fn);
}
 
TrafficProtocol.prototype.reset = function(){
  letsubject = this.subject;
 
  this.statePromise = Promise.resolve();
  this.stateList.forEach((stateFn) => {
    this.statePromise = this.statePromise.then(()=>{
      return new Promise(resolve => {
        stateFn(subject, resolve);
      });
    });
  });
  if(this.autoReset){
    this.statePromise.then(this.reset.bind(this));
  }
}
 
TrafficProtocol.prototype.start = function(){
  this.reset();
}
 
var traffic = new TrafficProtocol(trafficEl, true);
 
traffic.putState(function(subject, next){
  subject.className = "wait";
  setTimeout(next, 1000);
});
 
traffic.putState(function(subject, next){
  subject.className = "stop";
  setTimeout(next, 2000);
});
 
traffic.putState(function(subject, next){
  subject.className = "pass";
  setTimeout(next, 3000);
});
 
traffic.start();

這一版本里,我們設計了一個 TrafficProtocol 類,它有 putState、reset、start 三個方法:

  • putState 接受一個函數作為參數,這個函數自身有兩個參數,一個是 subject,是由 TrafficProtocol 對象初始化時設定的 DOM 元素,一個是 next,是一個函數,表示結束當前 state,進入下一個 state。
  • reset 結束當前狀態循環,開始新的循環。
  • start 開始執行循環,這里的實現是直接調用 reset。

看一下 reset 的實現思路:

TrafficProtocol.prototype.reset = function(){
  letsubject = this.subject;
 
  this.statePromise = Promise.resolve();
  this.stateList.forEach((stateFn) => {
    this.statePromise = this.statePromise.then(()=>{
      return new Promise(resolve => {
        stateFn(subject, resolve);
      });
    });
  });
  if(this.autoReset){
    this.statePromise.then(this.reset.bind(this));
  }
}

在這里我們創建一個 statePromise,然后將 stateList 中的方法(通過 putState 添加的)依次綁定到 promise 上。如果設置了 autoReset,那么我們在 promise 的最后綁定 reset 自身,這樣就實現了循環切換。

有了這個模型,我們要添加新的狀態,只需要通過 putState 添加一個新的狀態就好了。這一模型不僅僅可以用在這個需求里,還可以用在任何需要順序執行異步請求的地方。

最后,我們看到,版本六用到了面向對象、過程抽象、Promise等模式,它的優點是 API 設計靈活,通用性和擴展性好。但是版本六也有缺點,它的實現復雜度比前面的幾個版本都高,我們在做這樣的設計時,也需要考慮是否有 過度設計 的嫌疑。

總結

  • 設計是把雙刃劍,繁簡需要權衡,尺度需要把握。
  • 寫代碼簡單,程序設計不易,需要走心。

 

來自:http://web.jobbole.com/89753/

 

 本文由用戶 rjmorusejm 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!