当你编写对性能要求高的代码时, 考虑算法复杂度是个好办法, 用 Big-O 符号 表示.
Big-O 用来衡量 投入更多数据时代码会慢多少. 例如, 如果有个排序算法的复杂度是 O(n2), 排序 50 倍以上的数据大概要慢 502 = 2,500 时间. Big O 不会给出一个准确的数值, 但它可以帮助你知道算法 效果 如何.
一些例子: O(n), O(n log n), O(n2), O(n!).
然而, 这篇文章与算法或性能无关, 与 APIs 和调试有关. 事实证明, API 设计涉及到十分相似的考虑事项.
我们大部分时间都用于查找和修复代码中的错误, 大部分开发者希望可以更快的找到 bugs. 尽管最后的结果可能让人满意, 但当你已经制定好工作流程时, 花费一整天时间来找一个 bug 是很糟糕的.
调试经验会影响我们对抽象, 类库和工具的选择. 一些 API 和语言设计可以杜绝某类错误, 一些则会引发无数个错误, 可是我们怎么知道要选择哪个呢?
许多 APIs 的线上讨论主要是关于美学上的, 但其中没有太多提到实际使用后的感受.
我有一个指标可以帮助我思考这个问题, 我称它为 Bug-O 符号:
(n)
Big-O 描绘的是算法随着输入增长会变慢多少, Bug-O 描绘的是随着代码增长会变慢多少.
例如, 请思考下面代码, 随着时间流逝, 使用 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 https://overreacted.io/the-bug-o-notation/ (2019-01-25)
来源: https://juejin.im/post/5c67591de51d451b240a7a6f