2016年6月

今天写的一篇知乎专栏 知乎的话题屏蔽功能真是堪忧 里提到了一个「话题」被屏蔽时,其子孙(descendant)话题应该如何处理的问题。

事实上,知乎上存在着「关注一个话题能够看到其子孙话题的内容,屏蔽一个话题,却没有默认屏蔽其子孙话题」这样的不一致。也存在添加话题时的不一致:比如提问「茶党」相关的问题时,有人会选择在加上「茶党」话题的同时,也添加「美国政治」、「共和党」、「保守主义」等话题;也有人考虑到既然「茶党」是后面那些话题的子孙话题,于是关注了那些话题的用户自然会看到「茶党」话题下的问题,也就没有必要再另行添加那些话题。

类似的问题也一直困扰着我。

问题描述

每个话题/标签/分类,称之为一个 tag(分类)。Tag 之间可以互为父子关系,每个 tag 可以有多个父 tag,也可以有多个子 tag。Tag 间通常构成有向无环图。

每个问题/文章/A 片,称之为一个 entry(条目)。每个 entry 可以属于多个 tag。

这是一个很常见的问题,除了知乎的问题-话题外,WordPress 的文章-分类就符合这个问题,Wikipedia 的词条-分类也是。当然还有我的 A 片管理器中的视频-标签。

最简单是实现方式就是用四张数据表,一张存储 tag,一张存储 entry,一张存储 tag 和 entry 的对应关系,一张存储 tag 间的从属关系。

基本的情况

在我写 A 片管理器的时候,也遇到这个问题。最早,我也是在每次查询某个 tag 的 entry 时,递归查询其 descendant tag 的 entry。然而这样使得这个查询的速度慢了很多。

当然,这可以通过合适的索引和缓存来缓解。但由于 A 片管理器并没有一个持续运行的进程,而在硬盘上占用的空间也希望尽可能小,避免冗余,再加上我想让 A 片管理器本身小、简洁而可靠,所以我并不希望折腾这些。

另一方面,如果我要通过 tag 间的重合度来提示「相似的 entry」的话,事情就更加复杂了——比如 entry 1 属于 tag A,entry 2 属于 tag B,而 tag A 和 tag B 都同属 tag C 的 descendant 的情况下,实际上 entry 1 和 entry 2 是有一些相似性的,但实际上计算起来就会比较复杂。

自动添加父 tag

于是我改变了我的策略,采取了一个很独特的做法:把某个 tag 添加到一个 entry 时,自动向上遍历该 tag 的所有 ancestor,并且把他们都加到那个 entry 的 tag 列表里。

这样的话,就避免了对于同一个 entry,是否应该在添加 tag 的同时也添加其父 tag 的可能的不一致性。

代码写起来也轻松很多:查询一个 tag 下所有 entry 时,也快了不少——根本不用递归遍历其子 tag。更棒的是,查询两部 A 片的相似度时,直接计算他们的 tag 列表间的余弦相似度就行。

当添加 tag 间的从属关系的时候,我会启动一个后台线程自动补全每个 entry 的 tag 列表。譬如我把 tag D 设为 tag E 的子 tag 的时候,我会自动把所有属于 tag D 的 entry 也都自动加到 tag E 里面。

当然这样的解决方法是非常有局限性的。在数据量很大、用户可以随意更改 tag 从属关系的情况下,这个「后台线程」性能上很难支撑;同时进行多个 tag 从属关系的修改时如何保持一致性,也是极难解决的。不过对于我的 A 片管理器来说,不用考虑这些事情。

如果移除呢?

如果我此时又把 tag D 从 tag E 的子 tag 里面中移除,我是否该再遍历一遍刚才那些 entry,把 tag E 一一移除呢?或许不该,因为有些 entry 可能一开始就已经属于 tag E,而并非是刚才由后台线程添加进去的。

同样,如果我把 tag D 某个 entry 的 tag 列表中移除,是否也该移除 tag E 呢?

解决这个问题的办法是,追踪 tag E 到底是人为加上的,还是由于 tag 的从属关系而自动加上的。如果是前者,就不移除;如果是后者,则移除。

这有点像是软件包管理器管理依赖的方式——如果某个包是用户手动安装的,就不会被移除。而如果某个包是在安装其它包时由于依赖关系而自动安装的,就可以通过 apt-get autoremove 一类的方式来移除。当然这个标记也可以使用 apt-mark 一类的命令来更改("mark/unmark a package as being automatically-installed")。

这样的话,就完美解决了我 A 片管理器这个 case 里的所有问题。

Back in high school, I read a brilliant book, DOM Scripting (first edition) by Jeremy Keith. The Chinese translation of the book had a even better title: "JavaScript DOM 编程艺术", which literally means "the art of JavaScript DOM programming". The book was the first book I bought about JavaScript. It was so enlightening that it showed me how to write JavaScript in a "good" way (graceful degradation, in particular), and it eventually turned me into a frontend developer a decade later. If you ask to me to recommend 3 books for learning JavaScript, I'll definitely have DOM Scripting in the list.

Screenshot_2016-06-06_02-06-10.png

Last year, I saw a JavaScript function appeared in a question on SegmentFault, where the post author asked for some explanation on the code he posted. I wrote an answer:

The code you posted is a piece of shit. You can learn nothing from it. Actually, I've never seen any JavaScript code worse than that.

The post author replied: "I read the code from Keith's DOM Scripting". I was astonished. Recalling that DOM Scripting is such a masterpiece, I cannot believe it. After realizing the code was really extracted from the book, I felt embarrassed and woke up to the fact that JavaScript had really changed a lot over the years.

Screenshot_2016-06-06_02-03-54.png

Therefore, I made it a interview question for frontend developer candidates: read the very code section, and tell me how will you improve it. Only include basic code issues (including code style issues). Don't change the logic. Don't use fancy ES5/6 things. Don't say "requestAnimationFrame API" can help the performance. Just focus on the code itself. Here's the original code, again:

function moveElement(elementID,final_x,final_y,interval) {
    if (!document.getElementById) return false;
    if (!document.getElementById(elementID)) return false;
    var elem = document.getElementById(elementID);
    if (elem.movement) {
        clearTimeout(elem.movement);
    }
    if (!elem.style.left) {
        elem.style.left = "0px";
    }
    if (!elem.style.top) {
        elem.style.top = "0px";
    }
    var xpos = parseInt(elem.style.left);
    var ypos = parseInt(elem.style.top);
    var dist = 0;
    if (xpos == final_x && ypos == final_y) {
        return true;
    }
    if (xpos < final_x) {
        var dist = Math.ceil((final_x - xpos)/10);
        xpos = xpos + dist;
    }
    if (xpos > final_x) {
        var dist = Math.ceil((xpos - final_x)/10);
        xpos = xpos - dist;
    }
    if (ypos < final_y) {
        var dist = Math.ceil((final_y - ypos)/10);
        ypos = ypos + dist;
    }
    if (ypos > final_y) {
        var dist = Math.ceil((ypos - final_y)/10);
        ypos = ypos - dist;
    }
    elem.style.left = xpos + "px";
    elem.style.top = ypos + "px";
    var repeat = "moveElement('"+elementID+"',"+final_x+","+final_y+","+interval+")";
    elem.movement = setTimeout(repeat,interval);
}

Here are some possible answers I'll give, listed in the order they appeared:

  • Use var moveElement = function (...) syntax instead of function moveElement(...) to make the program more consistent (okay, it's just my personal preference).
  • Leave spaces after commas in argument list (i.e. moveElement(elementID, final_x, final_y, interval) instead of moveElement(elementID,final_x,final_y,interval)). Similarly, leave spaces around operators like /.
  • Either use camel case (better) or underscore case. Don't mix them up (elementID and final_x).
  • Always put curly brackets around the body of if, even when there is only one statement inside (return false in this example).
  • Always have consistent return type (don't return false at the beginning, but returning nothing at the end).
  • Don't declare variables using var inside if blocks. JavaScript's var keyword only gives function-level locality.
  • Don't use var to declare the same variable more than one time (there are var dist for multiple times in the code).
  • The is no point in assigning zero to dist at the beginning.
  • It's a very bad practice to pass a string as the argument to setTimeout. Pass a function instead.
  • Using == to compare is discouraged. Use === instead.
  • Some code quality tools (JSLint or JSHint) will ask you to supply the second argument (10) to parseInt.
  • Don't call to getElementById each time moveElement is called. Call getElementById just once, and use the element itself in recurring calls.
  • Similarly, don't call parseInt again and again. Put numeric values in variables instead.

Here's the revised code I'll have:

var moveElement = function (elementID, finalX, finalY, interval) {
    if (!document.getElementById) { return; }
    if (!document.getElementById(elementID)) { return; }
    var elem = document.getElementById(elementID);
    if (elem.movement) {
        clearTimeout(elem.movement);
    }
    var xpos = 0;
    var ypos = 0;
    var run = function () {
        var dist;
        if (xpos === finalX && ypos === finalY) {
            return;
        }
        if (xpos < finalX) {
            dist = Math.ceil((finalX - xpos) / 10);
            xpos = xpos + dist;
        }
        if (xpos > finalX) {
            dist = Math.ceil((xpos - finalX) / 10);
            xpos = xpos - dist;
        }
        if (ypos < finalY) {
            dist = Math.ceil((finalY - ypos) / 10);
            ypos = ypos + dist;
        }
        if (ypos > finalY) {
            dist = Math.ceil((ypos - finalY) / 10);
            ypos = ypos - dist;
        }
        elem.style.left = xpos + "px";
        elem.style.top = ypos + "px";
        elem.movement = setTimeout(run, interval);
    };
    run();
};

What will the frontend developer candidates answer? Maybe they're used to shims, polyfills, transpilers. Will they know the hard years we had with IE 6?

最近意识到很多 office politics 的事情。其实敝司挺多 office politics 的纷争,只是我恰巧都不用涉身其中。

所以说码农是幸运的,因为有自己的一亩三分地。自己的 code base 不容别人轻易更改,自己的 code style 可以保持 consistent。自己只要保证向上面按时交纳公粮,上面就不会找你麻烦,不会干涉你的具体工作。

而因为有这自己的一亩三分地产粮,如果运气不太差的话,也几乎不须去参与那些毫无意义的政治斗争,不须溜须拍马,唯唯诺诺;不须讨好谁,也不会得罪谁。

而产品经理乃至产品设计师就很不幸了。没有自己的一亩三分地,却必须要做出事情来给老板看,就不得不去别人的田地上收粮食,难免要淌浑水。尤其在公司威望不高的产品经理,简直惨不忍睹。近来公司里的各种冲突,大多源自于此。

所以公司找了两个 Android 程序员来给我打下手帮忙,我其实感到有负担。很多时候,他们写出来的代码我不满意,修改起来总是费力,但又不愿去和他自己解释(因为懒且怕尴尬)。而分配工作给他们,也常常成为让我头疼的事情。大约,我是在不喜欢有半点 management 性质的东西。大约,在我眼里,这样下来,已经不是完完全全的一亩三分地了。

可是,尘世里,又哪里去找真正一个人就能完成的工作呢?