算法复杂性“Bug-O”表示法 — Overreacted

栏目: JavaScript · 发布时间: 5年前

内容简介:在编写对性能敏感的代码时,最好记住它的算法复杂性。它通常用Big-O衡量代码在向其投入更多数据时会变慢多少。例如,如果排序算法具有O(一些例子:O(

在编写对性能敏感的代码时,最好记住它的算法复杂性。它通常用 Big-O表示法表示

Big-O衡量代码在向其投入更多数据时会变慢多少。例如,如果 排序 算法具有O( n 2 )复杂度,则排序×50倍以上的项目将大约为50 2 =慢2,500倍。Big O不会给你一个确切的数字,但它可以帮助你理解算法如何扩展。

一些例子:O( n ),O( n log  n ),O( n 2 ),O( n! )。

但是,这篇文章与算法或性能无关。它是关于API和调试的。事实证明,API设计涉及非常类似的考虑因素。

我们的大部分时间都用于查找和修复代码中的错误。大多数开发人员希望更快地发现错误。尽管最终可能令人满意,但是如果您已经实施了路线图中的某些内容,那么花费一整天时间来追逐单个错误是很糟糕的。

调试经验会影响我们对抽象,库和 工具 的选择。一些API和语言设计使得错误变得不可能。有些则会产生无穷无尽错误,如何分辨?

我有一个指标可以帮助我思考这个问题。我把它称为 Bug-O 表示法。

Big-O描述了随着输入增长,算法减慢了多少。Bug-O描述了随着你的代码的增长,API让你减缓多少。

例如,考虑一下这个代码,它会随着时间的推移使用node.appendChild(),node.removeChild()手动更新DOM,且这两个函数没有明确的结构:

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

这段代码的问题并不在于它“丑陋”。我们不是在谈论美学。问题是,如果此代码中存在错误,我不知道从哪里开始查找。

根据回调和事件触发的顺序,该程序可能采用的代码路径数量会出现组合爆炸。在其中一些中,我会看到正确的信息。在其他情况下,我会看到多个微调器,故障和错误消息,并可能崩溃。

此功能有4个不同的部分,不保证其顺序。我的非科学计算告诉我,他们可以运行4×3×2×1 = 24个不同的顺序。如果我再添加四个代码段,它将是8×7×6×5×4×3×2×1 - 四万组合。祝你好运调试。

换句话说,这种方法的Bug-O是BUg( n! ),其中n是触及DOM的代码段的数量。是的,这是一个因素。当然,我在这里并不是很科学。在实践中并非所有转换都是可能的。但另一方面,这些细分中的每一个都可以运行多次。

为了改进此代码的Bug-O,我们可以限制可能的状态和结果的数量。我们不需要任何库来执行此操作。这只是在我们的代码上强制执行某些结构的问题。这是我们可以做到的一种方式:

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

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

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

  currentState = nextState;
  <b>switch</b> (nextState.step) {
    <b>case</b> 'initial':
      <b>break</b>;
    <b>case</b> 'pending':
      formStatus.appendChild(spinner);
      <b>break</b>;
    <b>case</b> 'success':
      let successMessage = createSuccessMessage();
      formStatus.appendChild(successMessage);
      <b>break</b>;
    <b>case</b> 'error':
      let errorMessage = createErrorMessage(nextState.error);
      let retryButton = createRetryButton();
      formStatus.appendChild(errorMessage);
      formStatus.appendChild(retryButton);
      retryButton.addEventListener('click', trySubmit);
      <b>break</b>;
  }
}
</font>

此代码可能看起来不太相似。它甚至有点冗长。但由于这个思路,调试起来非常简单:

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

  </font><font><i>// ... the code adding stuff to formStatus ...</i></font><font>
</font>

通过在执行任何操作之前清除表单状态,我们确保我们的DOM操作始终从头开始。这就是我们如何对抗不可避免的 - 不要让错误累积起来。这是相当于“关闭再打开”的编码,它的效果非常好。

如果在输出中出现错误,我们只需要考虑一个退一步-以前的setState电话。调试渲染结果的Bug-O是Bug(n),其中n是渲染代码路径的数量。这里只有四个(因为我们在a中有四个案例switch)。

我们在设置状态时可能仍然存在竞争条件,但调试它们更容易,因为可以记录和检查每个中间状态。我们还可以明确禁止任何不需要的转换:

当然,总是重置DOM需要权衡。每次都过分删除和重新创建DOM会破坏其内部状态,失去焦点,并在较大的应用程序中导致可怕的性能问题。

这就是像React这样的库包可以提供帮助的原因。它们让您在总是从头开始重新创建UI的范例中思考,而不必这样做:

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

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

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

  <b>return</b> (
    <form onSubmit={handleSubmit}>
      {content}
    </form>
  );
}
</font>

代码可能看起来不同,但原理是相同的。组件抽象强制执行边界,以便您知道页面上没有其他代码可以混淆其DOM或状态。组件化有助于减少Bug-O。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序员修炼之道

程序员修炼之道

Andrew Hunt、David Thomas / 马维达 / 电子工业出版社 / 2011-1 / 55.00元

《程序员修炼之道:从小工到专家》内容简介:《程序员修炼之道》由一系列独立的部分组成,涵盖的主题从个人责任、职业发展,知道用于使代码保持灵活、并且易于改编和复用的各种架构技术,利用许多富有娱乐性的奇闻轶事、有思想性的例子及有趣的类比,全面阐释了软件开发的许多不同方面的最佳实践和重大陷阱。无论你是初学者,是有经验的程序员,还是软件项目经理,《程序员修炼之道:从小工到专家》都适合你阅读。一起来看看 《程序员修炼之道》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具