「Bug-O」 符號

當你編寫對性能要求高的代碼時,考慮算法複雜度是個好辦法,用Big-O 符號 表示。node

Big-O 用來衡量 投入更多數據時代碼會慢多少。例如,若是有個排序算法的複雜度是 O(n2),排序50倍以上的數據大概要慢 502 = 2,500 時間。Big O 不會給出一個準確的數值,但它能夠幫助你知道算法 效果 如何。react

一些例子:O(n), O(n log n), O(n2), O(n!)。算法

然而,這篇文章與算法或性能無關,與APIs和調試有關。 事實證實,API設計涉及到十分類似的考慮事項。app


咱們大部分時間都用於查找和修復代碼中的錯誤,大部分開發者但願能夠更快的找到bugs。儘管最後的結果可能讓人滿意,但當你已經制定好工做流程時,花費一成天時間來找一個bug是很糟糕的。ide

調試經驗會影響咱們對抽象、類庫和工具的選擇。一些 API 和語言設計能夠杜絕某類錯誤,一些則會引起無數個錯誤,但是咱們怎麼知道要選擇哪一個呢工具

許多APIs的線上討論主要是關於美學上的,但其中沒有太多提到實際使用後的感覺。組件化

我有一個指標能夠幫助我思考這個問題,我稱它爲 Bug-O符號post

🐞(n)性能

Big-O 描繪的是算法隨着輸入增加會變慢多少,Bug-O 描繪的是隨着代碼增加會變慢多少。ui


例如,請思考下面代碼,隨着時間流逝,使用 node.appendChild()node.removeChild() 這種着急地操做手動更新DOM,且結構不清晰:

function trySubmit() {
  // Section 1
  let spinner = createSpinner();
  formStatus.appendChild(spinner);
  submitForm().then(() => {
  	// Section 2
    formStatus.removeChild(spinner);
    let successMessage = createSuccessMessage();
    formStatus.appendChild(successMessage);
  }).catch(error => {
  	// Section 3
    formStatus.removeChild(spinner);
    let errorMessage = createErrorMessage(error);
    let retryButton = createRetryButton();
    formStatus.appendChild(errorMessage);
    formStatus.appendChild(retryButton)
    retryButton.addEventListener('click', function() {
      // Section 4
      formStatus.removeChild(errorMessage);
      formStatus.removeChild(retryButton);
      trySubmit();
    });
  })
}
複製代碼

代碼的問題不在於它 「醜」,咱們不討論美學,問題在於若是在代碼中存在一個bug,我不知道要從哪裏開始找

順序由回調和事件觸發決定,這個程序的代碼路徑數量能夠引起組合爆炸。可能最後我會看到正確的提示,也可能我會看到多個 spinners、失敗和錯誤提示同時出現或者代碼崩潰。

這個方法有4個部分且沒法保證它們的執行順序,我用很是不科學的方法計算,結果告訴我會有 4×3×2×1 = 24 種執行順序。若是我添加更多代碼塊,就多是 8×7×6×5×4×3×2×1 —— 四萬 種組合,祝你調試順利。

就是說,這示例中,Bug-O 爲🐞(n!),這裏 n 表示代碼中涉及DOM的代碼塊數量,這是個 階層。固然,這不是很科學的計算。在實際中,不可能全部的部分均可以轉換,但另外一方面,每一段均可以被重複使用,這樣 🐞(¯\(ツ)) 也許能更恰當些,但仍然不好勁,咱們能夠作得更好。


爲了改善這代碼的 Bug-O,咱們能夠減小可能用到的狀態和結果。咱們不須要任何類庫來實現,由於這只是個調整咱們代碼結構就能解決的問題,下面是咱們能夠用的一種方法:

let currentState = {
  step: 'initial', // 'initial' | 'pending' | 'success' | 'error'
};

function trySubmit() {
  if (currentState.step === 'pending') {
    // Don't allow to submit twice
    return;
  }
  setState({ step: 'pending' });
  submitForm.then(() => {
    setState({ step: 'success' });
  }).catch(error => {
    setState({ step: 'error', error });
  });
}

function setState(nextState) {
  // Clear all existing children
  formStatus.innerHTML = '';

  currentState = nextState;
  switch (nextState.step) {
    case 'initial':
      break;
    case 'pending':
      formStatus.appendChild(spinner);
      break;
    case 'success':
      let successMessage = createSuccessMessage();
      formStatus.appendChild(successMessage);
      break;
    case 'error':
      let errorMessage = createErrorMessage(nextState.error);
      let retryButton = createRetryButton();
      formStatus.appendChild(errorMessage);
      formStatus.appendChild(retryButton);
      retryButton.addEventListener('click', trySubmit);
      break;
  }
}
複製代碼

代碼可能看起來不難,不過它有點冗長。但因爲這行代碼,顯得調試起來簡單了些:

function setState(nextState) {
  // Clear all existing children
  formStatus.innerHTML = '';

  // ... the code adding stuff to formStatus ...
複製代碼

經過在執行任何操做以前清除表單狀態,以確保咱們的DOM始終從頭開始。這就是咱們解決不可避免的 —— 要讓錯誤累積起來。這就至關於 「關閉再打開」 的代碼,效果很是好。

若是輸出存在bug,咱們只須要回退 步 —— 前一次 setState 調用。這種代碼調試的 Bug-O 複雜度爲 🐞(n),n 是render分支的數量,在這是4(由於 switch 的分支是4)。

在狀態 賦值 中,咱們可能還須要一些條件判斷,但調試起來仍是挺容易的,由於能夠記錄和檢查每一個行進中的狀態值,咱們也能夠避免任何不想要的顯示轉換:

function trySubmit() {
  if (currentState.step === 'pending') {
    // Don't allow to submit twice
    return;
  }
複製代碼

固然,老是重置DOM是須要付出代價的,天真的每次都進行添加移除DOM操做會破壞內部狀態、失去焦點和應用變大時引發嚴重的性能問題。

這就是像 React 這樣的類庫的做用所在,它們使你從建立UI開始就能夠不用 擔憂 這些問題:

function FormStatus() {
  let [state, setState] = useState({
    step: 'initial'
  });

  function handleSubmit(e) {
    e.preventDefault();
    if (state.step === 'pending') {
      // Don't allow to submit twice
      return;
    }
    setState({ step: 'pending' });
    submitForm.then(() => {
      setState({ step: 'success' });
    }).catch(error => {
      setState({ step: 'error', error });
    });
  }

  let content;
  switch (state.step) {
    case 'pending':
      content = <Spinner />;
      break;
    case 'success':
      content = <SuccessMessage />;
      break;
    case 'error':
      content = (
        <>
          <ErrorMessage error={state.error} />
          <RetryButton onClick={handleSubmit} />
        </>
      );
      break;
  }

  return (
    <form onSubmit={handleSubmit}>
      {content}
    </form>
  );
}
複製代碼

代碼可能看起來不一樣,但原理是同樣的。組件抽離出可能遇到的問題,因此你知道不會有別的代碼弄亂內部的DOM或state。組件化有助於減少Bug-O。

實際上,若是 React App 中 任何 值看起來有問題,你能夠經過在React樹逐個查看組件上的代碼跟蹤它的來源。無論 app 有多大,跟蹤的值都等於 Bug-O🐞(樹高)。

你下次看見API討論時,請考慮:常見的調試任務的 🐞(n) 是多少?你當前熟悉的APIs和原則怎麼樣?Redux、CSS、繼承 —— 它們都有本身的 Bug-O。


翻譯原文The 「Bug-O」 Notation(2019-01-25)

相關文章
相關標籤/搜索