---
title: 一行代码千般喜
date: '2019-04-11'
slug: one-loc-joy
---

几年前我说过一个《[两个字符千行泪](/cn/2014/04/two-characters-endless-tears/)》的故事。我在谈论软件开发时，似乎谈论的忧桑故事多一些，可能是因为别扭的事不吐不快，而喜悦的事情则可以暗自独享。今日挑选一行今年我自认为比较得意的代码，参见 GitHub 上的[这则改动](https://github.com/yihui/servr/commit/faa6e52a02a)。这一行代码扫除了一个恶心了我自己两年多的问题。我把它敲出来的时候，觉得自己过去两年可能是脑子被驴踢过，为何没早想到这个呢。

还得交代一下背景故事，不然客官们几乎不大可能看懂这行代码的意义。在 servr 包中，我有一个监听本地文件变化的服务器函数 `servr::httw()`，这在各类服务器程序里很常见，功能就是当服务的文件（HTML/CSS/JS/图片之类的）有更新时，就通知浏览器自动刷新页面。这就省得你每次在编辑器里编辑文件后，还得挪动鼠标去浏览器里点刷新按钮查看你的改动。你只需要把浏览器丢在旁边，只在编辑器里编辑、保存文件即可。术语上这叫实时刷新（Live-reload）。

这个实现并不困难，我在《[用 R 创建网页服务器](/cn/2018/09/r-web-server/)》一文中已经略有提及，就是通过 WebSocket 让 R 与浏览器通信。浏览器每隔一段时间（比如一秒）就问问 R：你那边的文件有没有变动？R 用 `list.files()` 和 `file.info()` 算一下，若回答有，那么浏览器端就执行 JS 代码 `location.reload()` 刷新页面。这就是大致原理。

因为 R 需要比较当前文件信息和上一次的文件信息，所以我们需要把上一次的文件信息保存在一个变量里。前面提的那行代码中的双箭头就是干这事的：

```r
info <<- info2
```

其中 `info` 是外层变量，用来记录上一次的文件信息，`info2` 是内层变量，计算当前文件信息。那以前是什么问题恶心着我自己呢？这问题是，我要实现动态编译 R Markdown 文档；当 Rmd 文件更新时，我要先把它重新编译为 HTML，然后再在浏览器里刷新这个 HTML 文件。问题就出在编译上：因为编译可能会出错，比如 R 代码还没写完整就保存导致报错；报错之后我不能下一秒再继续重新编译，因为用户可能在一秒内还修复不了错误，所以过去我用的一个机制是模仿了 Gmail 掉线之后重试的时间间隔机制，也就是先等两秒再重试，两秒后还有问题再等四秒，然后等八秒、十六秒……直到错误被修正、文档能正确编译出来。

刚开始实现这个机制时，我甚至都快为自己的机智手动点赞了，我特么真呀么是个天才呀我，居然联想到了借鉴 Gmail 的办法。然而等我真的在自己 R Markdown 文档中不慎搞出个错误来时，我立刻就觉得自己快把自己蠢哭。看到红色的错误消息每隔一秒、两秒、四秒、八秒……一次次冒出来的时候，我彻底心方了，只能手忙脚乱赶紧思考我是谁、我从哪里来、晚饭要吃什么、到底哪里写错了、咋修正，比考试还紧张。

两个月前我在捣鼓[究极无限月读](/cn/2019/03/influence-focus/)时，突然想到：我特么为什么要不管不顾地重试编译一个有错误的文档呢？如果编译出错，那就先等着用户修改好文档保存之后再重试呀，骚年！他要是没重新保存，那我就该按兵不动，让他有充足的时间去找出错误并修正。

之前之所以会一遍遍重试编译，是因为我在重编译之前没有刷新一下上次的文件信息变量，即上面的 `info`，而是等编译成功之后才刷新，这就导致每当出错时，上一次的文件信息还是旧的（出错前的），而下次当浏览器问 R 的时候，R 仍然觉得文件有变动，所以不断重新编译一个错文档。这个问题的修正就是那么简单：在每次尝试重编译之前就刷新一下文件信息变量，成功后也照常刷新一遍，所以加一行代码就搞定了。

除了不再让用户感到莫名紧张，这个改动还让我成功扔掉了[一大坨当年我觉得聪明的代码](https://github.com/yihui/servr/commit/f34319f3)（也就是模仿 Gmail 的代码）。对码农而言，删代码永远都比添代码舒心得多。也是这个原因，今年我在 rticles 包中删掉了无数行代码（[例一](https://github.com/rstudio/rticles/commit/d6748afc10)、[例二](https://github.com/rstudio/rticles/commit/2b5091ed)），觉得终于把这个包归置到可维护的程度了。之前我们厂长写的代码冗余度高得令人发指，我忍了好几次终于忍不住了，大刀阔斧砍瓜切菜抽丝剥茧，把它调理成了逻辑基本统一的包，往后别人贡献新模板也更简单了。

尽管我不是故意制造问题再解决问题，但这千般喜实质上跟[弗洛伊德说的](/cn/2018/12/craving/)那种“在寒冷的夜晚把脚伸出被窝冻一会儿再缩回来的幸福”一样。说到底，只不过是我自己先蠢到一个极致（就是蠢到觉得还有点小聪明的程度），最后又发现这个蠢可以轻易解除，从而人为造就了满足感。
