<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Zxch3n's Blog]]></title><description><![CDATA[Zixuan Chen's Blog]]></description><link>https://www.zxch3n.com</link><generator>GatsbyJS</generator><lastBuildDate>Wed, 06 Nov 2024 06:14:24 GMT</lastBuildDate><item><title><![CDATA[About Me]]></title><description><![CDATA[About Me I'm the founder of  Loro . We build tools for local-first development and collaboration. Currently, my main focus is on this…]]></description><link>https://www.zxch3n.com/about/</link><guid isPermaLink="false">https://www.zxch3n.com/about/</guid><content:encoded>&lt;h1 id=&quot;about-me&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#about-me&quot; aria-label=&quot;about me permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;About Me&lt;/h1&gt;&lt;p&gt;I’m the founder of &lt;a href=&quot;https://loro.dev&quot;&gt;Loro&lt;/a&gt;. We build tools for local-first development and collaboration.&lt;/p&gt;&lt;p&gt;Currently, my main focus is on this startup company. I aim to create the best local-first collaboration tool, one that users can easily use, trust, and rely on for a lifetime.&lt;/p&gt;&lt;p&gt;I’ve been writing code for over a decade, and it’s often something that brings me joy. My main areas of interest are Rust, CRDTs, and text editors.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Loro Debug 日志 20240925]]></title><description><![CDATA[这个 Bug 出现在我试图用 Loro 来导入整个 Git Repo 的时候出现。分析了好几个小时最终才定位到 Bug，希望以后不要再有这种体验了，浅浅记录一下。 这 Bug 是以让人一头雾水的形态出现的。 首先背景是 Loro 支持记录 DAG…]]></description><link>https://www.zxch3n.com/debug-20240925/</link><guid isPermaLink="false">https://www.zxch3n.com/debug-20240925/</guid><pubDate>Wed, 25 Sep 2024 22:00:02 GMT</pubDate><content:encoded>&lt;ul&gt;&lt;li&gt;这个 Bug 出现在我试图用 Loro 来导入整个 Git Repo 的时候出现。分析了好几个小时最终才定位到 Bug，希望以后不要再有这种体验了，浅浅记录一下。&lt;/li&gt;&lt;li&gt;这 Bug 是以让人一头雾水的形态出现的。&lt;/li&gt;&lt;li&gt;首先背景是 Loro 支持记录 DAG 的操作历史结构，于是我想让它能够支持同步整个 Workspace 的内容，像 Git 一样。于是我这样设计：&lt;ul&gt;&lt;li&gt;根文档：是一片 Loro Doc，内部用 Tree 的结构存储“目录”信息。它会保存整个仓库的 Git 历史，但它不会记录每个文件的具体修改，而是记录这些文件修改前后的具体版本。具体方式是把每个文件夹和文件都当作 CRDT Tree 上的一个节点，这个节点会有 name 信息，是否是文件夹的信息，以及到它内容的指针；如果它是纯文本文档就会用 CRDT 文档的方式记录，并且会记录它的版本信息&lt;/li&gt;&lt;li&gt;内容指针：一个 Workspace 内的内容分成两种，一种是 CRDT 文档（用于同步纯文本信息），一种是二进制文件；所以在内容指针上也分成两种，分别指向不同的内容。&lt;/li&gt;&lt;li&gt;纯文本 CRDT 文档：会在自己的 CRDT 文档的 DAG 保存自己这个文件的完整的编辑历史&lt;/li&gt;&lt;li&gt;二进制文件：对于所有没办法转成合法 utf8 的内容都会直接用覆盖方式进行更新&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;最开始报的错误是根文档中 CRDT Tree 上的节点的 &lt;code class=&quot;language-text&quot;&gt;name&lt;/code&gt; 没有被设置。为了检查这个问题，我把所有对节点初始化的操作收束到一起，让它一定会在初始化的时候就设置好 name。但是无济于事，还是会有这个错误。加了相关的 Log 之后，发现这个节点 ID 就是被设置过 name。&lt;/li&gt;&lt;li&gt;会不会是因为出现了重复的 Peer ID 导致了呢？在导入 Git Repo 过程中，有时有些情况会允许我们重用 Peer ID，所以做了一些优化。但这个 bug 的出现让我对这些优化都产生了不自信，于是把它们关了，但是 bug 依旧存在。&lt;/li&gt;&lt;li&gt;我还继续检查了是不是 rename/copy 导致的错误，会不会是 delete 导致的错误。但都一一排除了。&lt;/li&gt;&lt;li&gt;会不会是因为我加了能够在单文档的不同版本上进行编辑的能力 &lt;a href=&quot;https://github.com/loro-dev/loro/pull/473&quot;&gt;https://github.com/loro-dev/loro/pull/473&lt;/a&gt; 之后出了错？这个功能会不会和现有的 Tree 内部的设计的 Invariants 产生冲突了？但思来想去没有想到合理的解释。&lt;/li&gt;&lt;li&gt;那还有一种可能性是我最不愿意考虑的：loro 内部的 bug 导致当前状态错误。Loro 老的版本中不支持在 detached 模式下进行编辑，有可能因为它的加入导致有些以前没考虑过的 corner case 被暴露出来。我们切换版本的时候往往都是根据版本之间的差异计算完成的，如果这里的差异计算出错，那么文档的状态就可能会错误。检查这种错误的最简便的方法就是回放历史，从而就能对比确认当前版本的状态复合从头开始回放文档计算出来的状态。&lt;/li&gt;&lt;li&gt;于是我在会出现找不到 name 的情况的位置加了确认 state 正确性的检查，结果发现真的是这里出错！正确的文档上是不存在这个节点的，我们恰好多了一个节点。那就是 Loro 内部的错误，之前没有被暴露出来。于是我先进行了下面的检查&lt;ul&gt;&lt;li&gt;如果 DiffCalculator 不用复杂的优化会不会出错&lt;/li&gt;&lt;li&gt;看看多出来的节点是不是曾经被删除过 - 不是&lt;/li&gt;&lt;li&gt;确认 Consistency Check 本身的代码是不是正确的&lt;/li&gt;&lt;li&gt;确认 Loro 现在是能够通过本身自己的所有内部测试的&lt;/li&gt;&lt;li&gt;Frontiers 转 VersionVector 和 VV 转 Frontiers 的实现是正确的&lt;/li&gt;&lt;li&gt;(感慨如果能通过 Log 直接生成测试用例来回放现场就好了，复现起来挺麻烦的&lt;/li&gt;&lt;li&gt;测试了 Tree 的内部 State 的 Invariants 的正确性&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;上面这些检查都没有发现问题&lt;/li&gt;&lt;li&gt;随后的一个突破点来自添加了更多的 log 之后发现，出问题的那个节点它的节点 ID 大于当前的文档的 Version Vector，也就意味着这个节点在一次版本切换中本来应该被 Revert 掉的，结果没有发生这个 Revert。于是就顺着这个线索找问题。&lt;/li&gt;&lt;li&gt;我们的文档状态可以理解为是通过 Diff Calculator 来完成更新的，Diff Calculator 会通过事件告诉文档需要进行什么样的更新。随后我发现事件中存在这个文档的创建事件，但不存在 Revert 事件。那就意味着 Tree 相关的 Diff Calculator 上出问题了，Tree State 不背这个锅。&lt;/li&gt;&lt;li&gt;那到 Tree Diff Calculator 这一侧，它就是一棵保存了树的相关操作历史的树，当发生版本切换的时候就在它上面执行这些切换操作计算出 InternalDiff，然后交给 State 完成更新。这部分的代码相关的 invariants 很多，但是相关的自动检查不多，于是就加了一波检查，然后又再是读日志分析问题，把一些可能有问题的优化去掉，看看病症有没有好起来。&lt;/li&gt;&lt;li&gt;最后定位到了是一处优化上错误的过滤了 ops，导致有一个本应该被记录到 tree 中的 op 没有被记录，从而导致后续计算错误才产生了这个问题。将这个优化去掉之后整体就恢复正常了。&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;教训&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E6%95%99%E8%AE%AD&quot; aria-label=&quot;教训 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;教训&lt;/h1&gt;&lt;ul&gt;&lt;li&gt;目前 Fuzz 的覆盖率还不够高，没有覆盖到刚刚那种场景，未来要加强 Fuzz 的覆盖率&lt;/li&gt;&lt;li&gt;Tree Diff Calculator 内部的 Invariants 需要加更多的检查来保证行为正确性&lt;/li&gt;&lt;li&gt;要引入一些机制让 State 错误更容易暴露出来&lt;ul&gt;&lt;li&gt;例如提供对状态的 Hash，落盘前对内容做一次 Hash，在版本切换的时候检查 Hash 正确性&lt;/li&gt;&lt;li&gt;在合适的时机做完整性检查&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[The Cook and the Chef: Musk's Secret Sauce 书摘]]></title><description><![CDATA[最近读了一篇 Wait But Why 2015 年的长文， The Cook and the Chef: Musk’s Secret Sauce…]]></description><link>https://www.zxch3n.com/first-principles/</link><guid isPermaLink="false">https://www.zxch3n.com/first-principles/</guid><pubDate>Sun, 04 Sep 2022 16:10:00 GMT</pubDate><content:encoded>&lt;p&gt;最近读了一篇 Wait But Why 2015 年的长文，&lt;a href=&quot;https://waitbutwhy.com/2015/11/the-cook-and-the-chef-musks-secret-sauce.html&quot;&gt;The Cook and the Chef: Musk’s Secret Sauce&lt;/a&gt;, 讨论的是马斯克的秘方：他为什么做这些事，为什么能够做到这些事。以马斯克为切入点，文章主要讨论了人们的思考方式，醍醐灌顶，在此摘录一下。（原文实在太长，本文只是其中的一小部分核心的记录）&lt;/p&gt;&lt;h1 id=&quot;两种不同的地质学&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E4%B8%A4%E7%A7%8D%E4%B8%8D%E5%90%8C%E7%9A%84%E5%9C%B0%E8%B4%A8%E5%AD%A6&quot; aria-label=&quot;两种不同的地质学 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;两种不同的地质学&lt;/h1&gt;&lt;p&gt;1681年，英国神学家托马斯-伯内特出版了《地球的神圣理论》，他解释了地质学的工作原理：当6000年前，地球形成为一个完美的球体，表面是田园风光的土地，内部是水。但后来，当表面干涸后，在其表面形成了裂缝，释放了内部的大部分水。这造成了《圣经》中的大洪水，诺亚不得不在整个星期内处理。一旦事情稳定下来，地球就不再是一个完美的球体—地球的表面被扭曲，带来了下面的山脉、山谷和洞穴，整个地球上到处都是洪水受害者的化石。&lt;/p&gt;&lt;p&gt;神学的难题是如何调和大量看似古老的地球特征与《圣经》中详述的更短的地球时间线。对于当时的神学家来说，这是他们的广义相对论与量子力学之争的版本，而伯内特想出了一个可行的弦理论，将这一切统一在一个屋檐下。&lt;/p&gt;&lt;p&gt;这不仅仅是伯内特的问题。有足够多的理论在协调地质学与《圣经》经文之间的关系，以至于今天需要一个15000字的“洪水地质学”维基百科页面。&lt;/p&gt;&lt;p&gt;大约在同一时间，另一群思想家开始研究地质学难题：科学家。&lt;/p&gt;&lt;p&gt;对神学家来说，游戏的起始规则是：“事实：地球始于6000年前，曾经发生过一场席卷地球的洪水”，他们的谜题严格地在这个背景下进行。但是科学家们在开始游戏时根本就没有任何规则。这个谜题是一块白板，欢迎任何他们发现的观察和测量。&lt;/p&gt;&lt;p&gt;在接下来的300年里，科学家们建立了一个又一个理论，随着新技术带来新的测量类型，旧的理论被推翻，并被新的更新版本所取代。随着地球的明显年龄越来越长，科学界不断给自己带来惊喜。1907年，有一个巨大的突破，美国科学家贝特拉姆-博尔特伍德开创了通过放射性测定法破译岩石年龄的技术，该技术在岩石中找到具有已知放射性衰变率的元素，并测量这些元素中哪些部分保持完整，哪些部分已经转化为衰变物质。&lt;/p&gt;&lt;p&gt;放射性测年法将地球的历史倒退到几十亿年前，这为科学界带来了新的突破，如大陆漂移理论，这反过来又导致了板块构造学说。科学家们大展拳脚。&lt;/p&gt;&lt;p&gt;与此同时，洪水地质学家们却不以为然。对他们来说，科学界的任何结论都是没有意义的，因为他们一开始就在破坏游戏规则。地球的正式年龄不到6000年，所以如果放射性测年显示出其他情况，那就是一种有缺陷的技术，就这样。&lt;/p&gt;&lt;p&gt;但是，科学证据越来越有说服力，随着时间的推移，越来越多的洪灾地质学家接受了科学家的观点—也许他们的游戏规则就是错误的。&lt;/p&gt;&lt;p&gt;无论信仰在精神领域扮演什么角色，当我们寻求关于地球年龄、我们物种的历史、闪电的原因或宇宙中任何其他物理现象的答案时，大多数人都同意数据和逻辑是远比信仰和经文更有效的工具。&lt;/p&gt;&lt;p&gt;然而当涉及到我们思考的大部分方式、我们做决定的方式以及我们生活的方式时，我们更像神学家而不是科学家。&lt;/p&gt;&lt;h1 id=&quot;人们如何推理&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E4%BA%BA%E4%BB%AC%E5%A6%82%E4%BD%95%E6%8E%A8%E7%90%86&quot; aria-label=&quot;人们如何推理 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;人们如何推理&lt;/h1&gt;&lt;p&gt;在数学中，我们把公设称为“公理”，而公理是100%真实的。因此，当我们从公理中建立结论时，我们称它们为“证明”，它们也是100%真实的。&lt;/p&gt;&lt;p&gt;而在物理学上是没有公理的，这是有原因的：&lt;/p&gt;&lt;p&gt;我们可以把牛顿的万有引力定律称为公理—在很长一段时间内，它确实看起来像一个公理，但是当爱因斯坦出现并表明牛顿实际上在极端条件下是不成立的，而广义相对论无论如何都能工作。因此，你会称广义相对论为公理。但是，当量子力学出现并表明广义相对论在极小的范围内无法适用，需要一套新的定律来解释这些情况时，又会发生什么？&lt;/p&gt;&lt;p&gt;一个基本的推理看起来像这样&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;因为 A = B&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;且 B = C + D&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;所以 A = C + D&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;而在科学上它是这样的&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;因为(根据现有事实，可能是这样) A = B&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;且(根据现有事实，可能是这样) B = C + D&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;所以 (根据我所知的，它看起来是这样) A = C + D&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;假设是为了被测试而建立的。测试一个假设可以推翻它或加强它，如果它通过了足够的测试，它就可以升级为理论。&lt;/p&gt;&lt;p&gt;现如今我们看到洪水地质学时会觉得这种理论很愚蠢，但在当时的时代背景下这并不是愚蠢和不科学的。他们中的许多人在自己的领域中与科学地质学家同事一样有成就。那么为什么他们会得出一个错误的结论？原因就在于他们是宗教教条的受害者，他们被告知应不带质疑地相信这些教条。&lt;/p&gt;&lt;p&gt;洪水地质学家的推理方式是&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;因为 (根据教条) A = B&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;且 (根据教条) B = C + D&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;所以 (不容置疑的) A = C + D&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;甚至比起成为任何教条的受害者，洪水地质学家更是他们自己对教条的确定性的受害者。没有确定性，教条就没有力量。而当需要数据才能相信某些东西时，虚假的教条就没有了立足之地。阻碍洪水地质学家的不是教会的教条，而是教会基于信仰的确定性心态。&lt;/p&gt;&lt;p&gt;如霍金所说：“知识最大的敌人不是无知，而是知识的幻觉”。科学地质学家和洪水地质学家都不是一开始就有知识。但是，让科学地质学家有力量去寻找真相的是知道他缺乏知识。科学地质学家认同实验室的心态，即从“我什么都不知道”开始，然后再向上努力。&lt;/p&gt;&lt;h1 id=&quot;大多数人的思考方式&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%A4%A7%E5%A4%9A%E6%95%B0%E4%BA%BA%E7%9A%84%E6%80%9D%E8%80%83%E6%96%B9%E5%BC%8F&quot; aria-label=&quot;大多数人的思考方式 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;大多数人的思考方式&lt;/h1&gt;&lt;blockquote&gt;&lt;p&gt;被称为预先看到未来到来的人，实际上并不是因为他们能超脱现实，而是因为科学的思考方式以及孜孜不倦的学习，让他们更能看清现实。而主流的被大多数人所认可的“现实”，则是落后于这个时代的。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;每个人的成长方式不同，但对我认识的大多数人来说，情况是这样的：我们的父母和老师教给我们各种各样的东西—什么是正确和错误，什么是安全和危险，你应该和不应该成为什么样的人。但我们的想法是。我是一个成年人，所以我比你知道得更多，这是不容争论的，不要争论，只要服从。&lt;/p&gt;&lt;p&gt;孩子的本能不只是知道该做什么和不该做什么，她想了解她所处环境的规则。而要理解一个东西，你必须对这个东西的建造过程有一个认识。当父母和老师告诉孩子要做XYZ并简单服从时，这就像在孩子的脑子里安装了一个已经设计好的软件。当孩子们问“为什么”，然后问“为什么”，然后问“为什么”的时候，他们正试图解构这个软件，看看它是如何建成的—深入到下面的第一原理，这样他们就可以衡量自己究竟应该在多大程度上关心成人似乎如此坚持的东西。&lt;/p&gt;&lt;p&gt;孩子头几次玩“为什么 ”的游戏时，家长认为很可爱。但是，许多家长和大多数老师很快就想出了一个办法，把这个游戏切断。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;“因为我是这么说的。”&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;“因为我是这么说的”给孩子的解构工作插入了一个具体的底线，低于这个底线就不能再提“为什么”。它说：“你想要第一原理？就在这里。这就是你的底线。没有必要再有‘为什么’。现在他妈的把你的靴子穿上，因为我说了，我们走吧。”&lt;/p&gt;&lt;p&gt;平心而论，父母的生活很糟糕。他们必须做所有过去必须做的事情，只是现在还有这些好奇的小孩需要他们来照顾，他们认为父母的存在是为了服务他们。在一个繁忙的日子里，刨根问底的“为什么”游戏是一场噩梦。&lt;/p&gt;&lt;p&gt;但这可能是一个值得忍受的噩梦。一条命令、一个教训或一句智慧的话语，如果没有洞察到它所建立的逻辑步骤，就是填鸭，而不是教他们讲道理。当我们被这样教育的时候，我们最终得到的是一桶鱼，而没有鱼竿—一个我们已经学会如何使用的安装好的软件，但没有能力自己编写任何东西。&lt;/p&gt;&lt;p&gt;学校让事情变得更糟。我最喜欢的思想家之一，作家Seth Godin，在TED演讲中解释说，目前的教育系统是工业时代的产物，那是一个生产力和生活水平飞速提高的时代。但伴随着更多的工厂，需要更多的工厂工人，所以我们的教育系统是围绕这个目标重新设计的。他解释说：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;普及公共教育的唯一目的不是为了培养明天的学者—我们有很多学者。它是为了培养人们愿意在工厂工作。它是为了训练人们的行为，遵守规定，适应环境。“我们对你进行一整年的处理。如果你有缺陷，我们就把你留下来，再次处理你。我们让你们坐成一排，就像他们在工厂里组织东西一样。我们建立了一个关于可互换人员的系统，因为工厂是基于可互换的部件。”&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;这一概念可以与我的另一位喜爱的作家詹姆斯-克里尔最近在他的博客上解释的内容结合起来：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;在20世纪60年代，一位名叫乔治-兰德的创造性表现研究者对1600名5岁儿童进行了研究，98%的儿童得分在“高度创造性 ”范围内。兰德博士以五年为单位对每个受试者进行了重新测试。当这些孩子10岁的时候，只有30%的人得分在高度创造性的范围内。这个数字在15岁时下降到12%，到25岁时只有2%。当孩子们成长为成年人时，他们的创造力实际上已经被训练出来了。用兰德博士的话说，“非创造性的行为是后天习得的”。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;这很有道理，对吗？创造性思维是第一原理推理的一个近亲。在这两种情况下，思考者都需要发明自己的思维途径。人们认为创造力是一种天生的才能，但它实际上更像是一种思维方式—它是在空白画布上绘画的思维版本。但要做到这一点，需要大脑软件在想出新东西方面的熟练和实践，而学校训练我们的概念恰恰相反—跟着领导走，单线作战，并在考试中获得真正的优势。学校交给孩子们的不是一张空白的画布，而是一本涂色书，并告诉他们要保持在线内。&lt;/p&gt;&lt;p&gt;现在的教育方式很多时候是&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;因为 (老师/课本/家长说过): A = B&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;且 (老师/课本/家长说过): B = C + D&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;从而 (可以肯定): A = C + D&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;以这种思考方式生活，人们最终得到的是一套不一定基于他们自己深入思考的价值观，一套不一定基于他们所生活的世界的现实的关于世界的信念，以及一堆他们可能很难用诚实的心去捍卫的观点。&lt;/p&gt;&lt;img src=&quot;https://waitbutwhy.com/wp-content/uploads/2015/11/Because-I-said-so-21.jpg&quot; width=&quot;300&quot;/&gt;&lt;h1 id=&quot;新部落主义&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E6%96%B0%E9%83%A8%E8%90%BD%E4%B8%BB%E4%B9%89&quot; aria-label=&quot;新部落主义 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;新部落主义&lt;/h1&gt;&lt;p&gt;新部落主义：群体中虽然每个人都是独立的，但群体有时有它自己的价值观和观点，定义了每个人应该怎么思考和是非对错。这里的部落指的是一个个群体，比如某个党派、某个教会、某种兴趣小组。&lt;/p&gt;&lt;p&gt;部落主义的好坏取决于部落成员和他们与部落的关系。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;当部落和部落成员都有一个独立的身份，而且他们碰巧是相同的时候，部落主义是好的。&lt;/strong&gt;部落成员选择成为部落的一部分，因为这恰好符合他的真实身份。如果部落或成员的身份演变到两者不再匹配的地步，这个人就会离开部落。让我们把这称为有意识的部落主义。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;当部落和部落成员的身份是同一的时候，部落主义是不好的。&lt;/strong&gt;部落成员的身份是由部落的教条碰巧所说的任何东西决定的。如果部落的身份改变了，部落成员的身份也会随之改变，步调一致。部落成员的身份不能独立于部落的身份而改变，因为成员没有独立的身份。让我们把这称为&lt;strong&gt;盲目的部落主义&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;人类想要内部的稳定安全，对于一个在成长过程中对自己的独特性格感到动摇的人来说，部落和它的指导性教条是一个重要的生命线—一个提供全套人类意见和价值观的一站式商店。&lt;/p&gt;&lt;p&gt;人类也渴望确定性的舒适和安全，而在盲目的部落主义的群体思维中，最能体现信念的地方莫过于此。科学家以数据为基础的意见只有在她所拥有的证据中才是强有力的，而且本质上会发生变化，而部落教条主义则是一种信仰的练习，由于没有数据的约束，盲目的部落成员确信地相信他们所相信的。&lt;/p&gt;&lt;p&gt;我们讨论了为什么数学有证明，科学有理论。在生活中，我们也许应该限制自己的假设。但盲目的部落主义却有数学家的自信：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;因为（部落这样说）： A = B&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;且 （部落这样说）: B = C + D&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;从而，可以肯定：A = C + D&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;试一试吧。下次你和你所在群体的成员在一起时，表达你在某个话题上的心态变化。如果你是一个虔诚的基督徒，在教堂里告诉人们你不再确信有上帝的存在。如果你是博尔德的一名艺术家，在下一个晚餐聚会上解释你认为全球变暖实际上可能是一个自由主义的骗局。如果你是一个伊拉克人，告诉你的家人，你最近觉得自己很支持以色列。如果你和你的丈夫是坚定的共和党人，告诉他你对奥巴马医改的态度有所转变。如果你来自波士顿，告诉你的朋友你今年会支持洋基队，因为你喜欢他们现在的球员群。&lt;/p&gt;&lt;p&gt;如果你在一个具有完全确定的盲目心态的部落里，你可能会看到一个惊恐的表情。这不仅看起来是错误的，而且似乎是异端邪说。他们可能会生气，他们可能会热情地试图说服你，他们可能会切断对话—但不会有开放性的对话。而且由于身份与盲目部落主义的信仰如此交织在一起，这个人实际上可能会在事后感到与你不那么亲近。因为对于僵化的部落主义的人来说，共同的教条在他们的亲密关系中发挥着比他们可能认识到的更重要的作用。&lt;/p&gt;&lt;p&gt;我们世界上的大多数主要分歧都来自盲目的部落主义，而在光谱的极端—人们是受控的绵羊—盲目的部落主义可能导致可怕的事情。就像历史上的那些时代，几个有魅力的坏人仅仅通过展示力量和激情就能建立一支由忠诚的步兵组成的庞大军队。&lt;strong&gt;盲目的部落主义是我们最大规模的暴行的真正幕后黑手&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;我们中的大多数人可能不会加入纳粹党，因为在“盲目 &amp;lt;-&amp;gt; 理性”的光谱上我们中的大多数人并不处于极端盲目的那一端。但我认为我们中的许多人也不在另一端。相反，我们通常处于朦胧的中间地带。&lt;/p&gt;&lt;h1 id=&quot;解决方案&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88&quot; aria-label=&quot;解决方案 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;解决方案&lt;/h1&gt;&lt;p&gt;为了成为一个独立思考者，我们需要吸收三点认知：&lt;/p&gt;&lt;h2 id=&quot;1-你什么也不知道&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#1-%E4%BD%A0%E4%BB%80%E4%B9%88%E4%B9%9F%E4%B8%8D%E7%9F%A5%E9%81%93&quot; aria-label=&quot;1 你什么也不知道 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 你什么也不知道&lt;/h2&gt;&lt;p&gt;科学地质学家和洪水地质学家都不是一开始就有知识。但是，让科学地质学家有力量去寻找真相的是「他知道他缺乏知识」。科学地质学家认同实验室的心态，即从“我什么都不知道”开始，然后再以此往上努力。&lt;/p&gt;&lt;p&gt;如果你想看实践中的实验室心态，只要搜索一下任何著名科学家的名言，你就会看到他们每个人都在表达他们什么都不知道的事实。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;艾萨克-牛顿：“对我自己来说，我只是一个在沙滩上玩耍的孩子，而真理的浩瀚海洋在我面前还没有被发现”&lt;/li&gt;&lt;li&gt;理查德-费曼：“我生来就不知道，只有一点时间来改变这里和那里。”&lt;/li&gt;&lt;li&gt;尼尔斯-玻尔：“我说的每一句话都必须被理解为不是一种肯定，而是一个问题。”&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;这些聪明得令人发指的人之所以对自己的知识如此谦虚，是因为作为科学家，他们知道不合理的确定性是理解的祸根，是有效推理的死亡。他们坚信，各种推理都应该在实验室里进行，而不是在教堂里进行。&lt;/p&gt;&lt;p&gt;如果你和一辆汽车单独呆在一个房间里，想弄清楚它是如何工作的，你可能会从拆开它开始，检查各个部件以及它们是如何结合在一起的。为了对我们的思维做同样的事情，我们需要回到四岁的自我，通过重新玩“为什么”的游戏，开始解构我们的思考。现在是时候卷起袖子，打开引擎盖，用我们的双手去解决一些并不有趣的问题，比如我们真正想要什么，什么是真正可能的，以及我们的生活方式是否符合这些东西的逻辑。&lt;/p&gt;&lt;p&gt;对于每个问题，我们面临的挑战是不断地问为什么，直到你触及最底层—这个最底层会告诉你，你是在教堂还是在实验室。如果你碰到的地板是一个或多个第一性原理，代表了事实或你的内在自我，并且逻辑准确，你就在实验室里。如果一个“为什么”的路径碰到了一个叫做“因为「权威」这么说”的楼层—如果你一直往下走，并在底部意识到整个事情只是因为你接受了你父母、朋友、宗教或社会的说法，那么你就在教堂里。如果那个教会的信条不能真正与你产生共鸣或反映当前世界的现实—如果事实证明你一直在用错误的配方工作—那么无论在它上面建立了什么结论，都将是同样错误。正如洪水地质学家所证明的那样，一个推理链只有在其最薄弱的环节才会强大。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://waitbutwhy.com/wp-content/uploads/2015/11/False-Dogma-2-565x600.jpg&quot;/&gt;&lt;/p&gt;&lt;p&gt;你真正要仔细观察的是毫无道理的确定性。在生活中，你对某件事情的感觉是如此正确，以至于它没有资格成为一个假设，甚至是一个理论，但它感觉像是一个公理？当有公理级别的确定性时，这意味着要么有一些严肃的具体和经过验证的数据在下面，要么就是基于信仰的教条。也许你确信辞职会是一场灾难，或者确信没有上帝，或者确信上大学很重要，或者确信你在假期中总是过得很愉快，或者确信当你在小组聚会中拿出吉他时大家都喜欢它—但如果它没有来自事实的支持，它最多是一个假设，且有可能是一个完全错误的教条。&lt;/p&gt;&lt;p&gt;谦逊顾名思义就是一个起点—它让你从这里开始踏上旅程。确定性的傲慢既是一个起点也是一个终点—不需要旅程。这就是为什么我们以“我什么都不知道 ”开始是如此重要。那是我们知道我们在实验室里的时候。&lt;/p&gt;&lt;h2 id=&quot;2-其他人也什么都不知道&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#2-%E5%85%B6%E4%BB%96%E4%BA%BA%E4%B9%9F%E4%BB%80%E4%B9%88%E9%83%BD%E4%B8%8D%E7%9F%A5%E9%81%93&quot; aria-label=&quot;2 其他人也什么都不知道 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 其他人也什么都不知道&lt;/h2&gt;&lt;blockquote&gt;&lt;p&gt;当你长大后，你往往会被告知世界是这样的，你的生活就是在这个世界里过日子。尽量不要过多地敲打墙壁。试着有一个美好的家庭生活，有乐趣，存一点钱。那是一个非常有限的生活。一旦你发现一个简单的事实，生活可以更宽广。而这就是。你周围所有你称之为生活的东西都是由那些并不比你聪明的人编造出来的。而你可以改变它，你可以影响它，你可以建立你自己的东西，让其他人可以使用。一旦你学会了这一点，你就不会再像以前那样了。 —— 乔布斯&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;这是乔布斯的说法，“你可能什么都不知道。但没有人知道狗屎。如果皇帝在你眼里是裸体的，而其他人都说他有衣服，那就相信你的眼睛，因为其他人不知道你不知道的事情。”。&lt;/p&gt;&lt;p&gt;这是一个容易理解的信息，一个更难相信的信息，一个更难行动的信息。&lt;/p&gt;&lt;p&gt;不尊重社会与我们长大后所受的教育完全背道而驰—但如果你只看你的眼睛和经验所告诉你的，它就非常合理。&lt;/p&gt;&lt;p&gt;周围有很多线索告诉我们，传统智慧什么都不知道。&lt;strong&gt;传统智慧崇拜现状，总是假设一切都有很好的理由&lt;/strong&gt;，而历史就是一个长长的记录，每当有人来改变事物时，现状的教条就会一次次被证明是错误的。&lt;/p&gt;&lt;p&gt;如果你睁开眼睛，在你自己的生活中还有其他的线索，你所生活的社会是没有什么好怕的。所有你了解到一个公司内部的真实情况，并发现它是完全无组织和糟糕的运作。所有身居高位的人似乎都不能把他们的个人生活安排好。&lt;/p&gt;&lt;p&gt;第一条认知是关于粉碎傲慢的保护壳，以奠定谦卑的起点。第二条是关于自信—通过一条建立在第一原则上而不是类比上的途径从这种谦卑中走出来的自信。这是一种自信，它说：“我可能知道的不多，但其他人也不知道，所以我不妨成为地球上最有知识的人。”&lt;/p&gt;&lt;h2 id=&quot;3-你的人生是一场游戏&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#3-%E4%BD%A0%E7%9A%84%E4%BA%BA%E7%94%9F%E6%98%AF%E4%B8%80%E5%9C%BA%E6%B8%B8%E6%88%8F&quot; aria-label=&quot;3 你的人生是一场游戏 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 你的人生是一场游戏&lt;/h2&gt;&lt;p&gt;科学的方法是通过测试假设来学习，而假设的建立是为了被推翻，这意味着科学家通过失败学习。失败是他们过程中的一个关键部分。&lt;/p&gt;&lt;p&gt;许多最有影响力的人似乎把世界当作实验室，把他们的生活当作实验课—这是成功的最佳途径。&lt;/p&gt;&lt;p&gt;但对我们大多数人来说，我们就是做不到。是什么阻止了我们？我认为主要有两点错误认知&lt;/p&gt;&lt;h3 id=&quot;错误认知-1-错位的恐惧&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E9%94%99%E8%AF%AF%E8%AE%A4%E7%9F%A5-1-%E9%94%99%E4%BD%8D%E7%9A%84%E6%81%90%E6%83%A7&quot; aria-label=&quot;错误认知 1 错位的恐惧 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;错误认知 1: 错位的恐惧&lt;/h3&gt;&lt;p&gt;人类被设定为非常认真地对待恐惧，而进化论认为让我们评估和重新评估我们内心的每个恐惧是没有效率的。它转而采用了“安全比遗憾好”的哲学—即如果某种恐惧有可能是基于真正的危险，就把它作为真正的恐惧，即使你的恐惧没有根据，也要把它留在身边，以防万一。安全总比遗憾好。&lt;/p&gt;&lt;p&gt;所有这些恐惧的目的是为了让我们保护自己免受危险。我们的问题是，就进化论而言，危险=损害你的基因继续发展的机会的东西，即危险=不交配或死亡或你的孩子死亡，仅此而已。&lt;/p&gt;&lt;p&gt;因此，我们对各种形状和大小的恐惧的痴迷可能在5万年前的埃塞俄比亚为我们提供了很好的服务，但它毁了我们今天的生活。&lt;/p&gt;&lt;p&gt;因为它不仅将我们的恐惧普遍提高到“该死的，我们打猎失败了，现在孩子们这个冬天都要饿死了”的水平，尽管我们生活在一个“哦，不，我被解雇了，现在我必须在20度的理想温度下，用一个羽毛枕头在我父母家睡两个月”的世界，而且它还使我们对错误的事情感到害怕。我们更害怕公开演讲而不是在高速公路上发短信，更害怕在酒吧里接近一个有吸引力的陌生人而不是嫁给一个错误的人，更害怕无法负担和我们的朋友一样的生活方式而不是在毫无意义的事业中度过50年。&lt;/p&gt;&lt;h3 id=&quot;错误认知-2-错位的自我定位&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E9%94%99%E8%AF%AF%E8%AE%A4%E7%9F%A5-2-%E9%94%99%E4%BD%8D%E7%9A%84%E8%87%AA%E6%88%91%E5%AE%9A%E4%BD%8D&quot; aria-label=&quot;错误认知 2 错位的自我定位 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;错误认知 2: 错位的自我定位&lt;/h3&gt;&lt;p&gt;在社会上，当你在价值观方面、时尚方面、宗教方面、事业方面做出一些尝试时，你就给自己打上了烙印。由于人们喜欢把人简化，以便在他们自己的头脑中理解事物，你周围的群体通过把你放在一个明确标记的、过度简化的盒子里来标识你。&lt;/p&gt;&lt;p&gt;这让改变变得非常痛苦。改变令人厌恶，如果身份需要随之变化。而其他人也不会让事情变得简单。盲目的群体成员不喜欢其他成员的改变—这使他们感到困惑，迫使他们重新调整头脑中的信息，并威胁到群体的简单确定性。因此，进化的尝试往往会遭到反对、嘲弄或愤怒。&lt;/p&gt;&lt;p&gt;当你很难改变的时候，你就会依恋你现在的身份和你现在做的事情—依恋到模糊了科学家和实验之间的区别，你忘记了它们是两种不同的东西。&lt;/p&gt;&lt;p&gt;我们谈到了为什么科学家欢迎关于他们实验的负面反馈。但是，当你是实验本身的时候，负面反馈并不是一个新的、有帮助的信息，而是一种侮辱。而且它很伤人。它让你生气。而且，由于改变感觉是不可能的，无论如何，反馈也没有什么好处—这就像给父母就他们一个月大的孩子的名字提供负面反馈。&lt;/p&gt;&lt;p&gt;我们讨论了为什么科学家期望他们的大量实验会失败。但是，当你和实验是一体的时候，承担一个新的目标不仅是身份的改变，而且是将你的身份置于危险之中。如果实验失败了，你就失败了。你是一个永远的失败者。&lt;/p&gt;&lt;p&gt;我和马斯克谈到了美国，以及先辈们在建立这个国家时按照第一原则进行推理的方式。他说，他认为他们之所以能这样做，是因为他们有一块新的石板可以使用。那个时代的欧洲国家要想做这样的事情会困难得多—因为，正如他告诉我的，他们“被困在自己的历史中”。&lt;/p&gt;&lt;p&gt;我听到马斯克用这句话来描述今天的大型汽车和航空航天公司。他认为特斯拉和 SpaceX s就像18世纪末的美国—新鲜的新实验室，准备进行实验—但当他看向他们行业的其他公司时，他看到的是无法从一种干净的心态来推动他们的战略。马斯克在提到航空航天业时说：“有一种反对承担风险的巨大偏见。每个人都在努力优化而减少自己的风险”。&lt;/p&gt;&lt;p&gt;困在你的历史中意味着你不知道如何改变，你已经忘记了如何创新，你被困在世界给你的身份框中。&lt;/p&gt;&lt;p&gt;正是因为这个原因，史蒂夫-乔布斯回顾了他在1986年被苹果公司解雇的情况，认为这是一种变相的祝福。他说：“被苹果公司解雇是发生在我身上最好的事情。成功的沉重感被重新成为初学者的轻松感所取代。它使我得以进入我生命中最有创造力的时期之一。” 被解雇使乔布斯从他自己的历史枷锁中“解放”出来。&lt;/p&gt;&lt;p&gt;这些都是我认为世界上许多最能干的人被困在生活中的原因。&lt;/p&gt;&lt;p&gt;这最后一个顿悟的挑战是以某种方式找出一种方法来失去对你自己恐惧的尊重。这种尊重存在于我们的基因中，而削弱这种尊重的唯一方法是藐视它，并在最终没有坏事发生时看到“你所感受到的大部分恐惧只是烟雾弹“。在你的舒适区之外做一些事，如果结果并没有想象中那么可怕，那这会是一个令人难以置信得强大的经验，一个改变你的经验—每次你有这样的经验，它就会削去你对你的大脑根深蒂固的、非理性的恐惧的尊重。&lt;/p&gt;&lt;p&gt;因为最具创造性的人物和其他人最主要的认知差异在于前者知道：现实生活和《侠盗猎车手》实际上并没有什么不同。《侠盗猎车手》是一个有趣的视频游戏，因为它是一个虚假的世界，在那里你可以毫无顾忌地做一些事情。在高速公路上以200英里/小时的速度行驶。闯入一栋大楼。用你的车轧死一个人。在《GTA》中一切都没有限制。&lt;/p&gt;&lt;p&gt;与GTA不同，在现实生活中，有法律，有监狱。但这就是最大的差异。如果有人给你一个完美的模拟当今世界的游戏，并告诉你这一切都是假的，没有实际的后果—唯一的规则是你不能违反法律或伤害任何人，而且你仍然必须确保支持你和你的家人的基本需求—你会怎么做？我的猜测是，大多数人会做各种他们在现实生活中很想做但不敢尝试的事情，他们最终会很快在模拟中获得一个比他们目前的现实生活更成功、更真实的生活。消除恐惧和对身份或他人意见的担忧，就会把这个人推入实际上并不危险的实验室，让他们在舒适区之外的所有令人振奋的地方蹦蹦跳跳，他们的生活就会腾飞。这就是非理性的恐惧阻碍我们的生活。&lt;/p&gt;&lt;p&gt;当我看到我们这个时代令人惊叹的人物时，很清楚的是，他们或多或少地把现实生活当成了《侠盗猎车手》。而这样做给了他们超能力。这就是我认为史蒂夫-乔布斯所说的“Stay hungry. Stay foolish”。&lt;/p&gt;&lt;p&gt;而这正是第三个顿悟的内容：无畏。 &lt;/p&gt;</content:encoded></item><item><title><![CDATA[译：本地优先软件 Local-first software]]></title><description><![CDATA[You own your data, in spite of the cloud. 本文译自 Ink&Switch 发表于 2019 年 4 月的  《Local-first software》   This article is the translation of…]]></description><link>https://www.zxch3n.com/local-first/</link><guid isPermaLink="false">https://www.zxch3n.com/local-first/</guid><pubDate>Sat, 20 Aug 2022 22:00:02 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;You own your data, in spite of the cloud.&lt;/strong&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;本文译自 Ink&amp;amp;Switch 发表于 2019 年 4 月的 &lt;a href=&quot;https://www.inkandswitch.com/local-first/&quot;&gt;《Local-first software》&lt;/a&gt; &lt;/p&gt;&lt;p&gt;This article is the translation of &lt;em&gt;&lt;a href=&quot;https://www.inkandswitch.com/local-first/&quot;&gt;Local-first software&lt;/a&gt; written by Ink&amp;amp;Switch in April 2019&lt;/em&gt; &lt;/p&gt;&lt;p&gt;原文作者&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://martin.kleppmann.com/&quot;&gt;Martin Kleppmann&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://adamwiggins.com/&quot;&gt;Adam Wiggins&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://twitter.com/pvh&quot;&gt;Peter van Hardenberg&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://markmcgranaghan.com/&quot;&gt;Mark McGranaghan&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/blockquote&gt;&lt;p&gt;Google Docs 和 Trello 这样的云应用很受欢迎，因为它们能够实现与同事的实时协作，而且它们使我们能够轻松地从所有的设备上访问我们的工作。然而，因为数据集中在服务器上存储，云应用也从用户手中夺走了所有权和代理权。如果一个服务关闭了，软件就会停止运作，用该软件创建的数据也会丢失。&lt;/p&gt;&lt;p&gt;在这篇文章中，我们提出了 “本地优先软件” : 它是一套兼顾了协作和用户对数据的所有权的软件原则。本地优先的愿景包括离线可用和多端协作的能力，同时也改善数据的安全性、隐私性、长期可用性和用户对数据的控制能力。&lt;/p&gt;&lt;p&gt;我们调查了现有的数据存储和共享方法，从电子邮件附件到 Web Apps，再到由 Firebase 驱动的移动应用。我们分析了他们各自的特性。我们研究了无冲突复制数据类型（CRDTs）：这种数据结构从本质上就支持多用户，同时也是本地化的和保证私密性的。CRDTs 有可能成为实现本地优先的软件的基础技术。&lt;/p&gt;&lt;p&gt;本文分享了几年来我们在 &lt;a href=&quot;https://www.inkandswitch.com/&quot;&gt;Ink &amp;amp; Switch&lt;/a&gt; 开发本地优先软件原型的一些发现。这些实验检验了 CRDTs 在实践中的可行性，并探讨了这种新数据模型的 UI 上的挑战。最后，我们分别针对研究员、应用开发者和创业者提出了向本地优先软件发展的行动建议。&lt;/p&gt;&lt;p&gt;这篇文章还以 &lt;a href=&quot;https://www.inkandswitch.com/local-first/static/local-first.pdf&quot;&gt;PDF 格式&lt;/a&gt;发表在 &lt;a href=&quot;https://2019.splashcon.org/track/splash-2019-Onward-Essays&quot;&gt;Onward! 2019 年会议&lt;/a&gt;。请用以下方式引用&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Martin Kleppmann, Adam Wiggins, Peter van Hardenberg, and Mark McGranaghan. Local-first software: you own your data, in spite of the cloud. 2019 ACM SIGPLAN International Symposium on New Ideas, New Paradigms, and Reflections on Programming and Software (Onward!), October 2019, pages 154–178. &lt;a href=&quot;https://doi.org/10.1145/3359591.3359737&quot;&gt;doi:10.1145/3359591.3359737&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#%E5%8A%A8%E6%9C%BA%E5%8D%8F%E4%BD%9C%E5%92%8C%E6%89%80%E5%B1%9E%E6%9D%83&quot;&gt;动机：协作和所属权&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E6%9C%AC%E5%9C%B0%E4%BC%98%E5%85%88%E8%BD%AF%E4%BB%B6%E7%9A%84%E4%B8%83%E4%B8%AA%E6%84%BF%E6%99%AF&quot;&gt;本地优先软件的七个愿景&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#1-%E5%8D%B3%E5%88%BB%E5%8A%A0%E8%BD%BD%E6%95%B0%E6%8D%AE%E8%A7%A6%E6%89%8B%E5%8F%AF%E5%8F%8A&quot;&gt;1. 即刻加载：数据触手可及&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#2-%E5%A4%9A%E7%AB%AF%E5%90%8C%E6%AD%A5&quot;&gt;2. 多端同步&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#3-%E7%BD%91%E7%BB%9C%E5%8F%AA%E6%98%AF%E4%B8%80%E7%A7%8D%E5%8F%AF%E9%80%89%E9%A1%B9&quot;&gt;3. 网络只是一种可选项&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#4-%E6%97%A0%E7%BC%9D%E5%8D%8F%E4%BD%9C&quot;&gt;4. 无缝协作&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#5-%E9%95%BF%E6%9C%9F%E5%8F%AF%E7%94%A8&quot;&gt;5. 长期可用&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#6-%E5%AE%89%E5%85%A8%E6%80%A7%E5%92%8C%E9%9A%90%E7%A7%81%E6%80%A7&quot;&gt;6. 安全性和隐私性&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#7-%E4%BD%A0%E4%BF%9D%E7%95%99%E6%9C%80%E7%BB%88%E7%9A%84%E6%89%80%E6%9C%89%E6%9D%83%E5%92%8C%E6%8E%A7%E5%88%B6%E6%9D%83&quot;&gt;7. 你保留最终的所有权和控制权&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E7%8E%B0%E6%9C%89%E7%9A%84%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8%E5%92%8C%E5%85%B1%E4%BA%AB%E6%A8%A1%E5%BC%8F&quot;&gt;现有的数据存储和共享模式&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F%E6%9E%B6%E6%9E%84%E5%A6%82%E4%BD%95%E5%BD%B1%E5%93%8D%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C&quot;&gt;应用程序架构如何影响用户体验&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#%E6%96%87%E4%BB%B6%E5%92%8C%E9%82%AE%E4%BB%B6%E9%99%84%E4%BB%B6&quot;&gt;文件和邮件附件&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E7%BD%91%E9%A1%B5%E5%BA%94%E7%94%A8google-docs-trello-figma-pinterest-etc&quot;&gt;网页应用：Google Docs, Trello, Figma, Pinterest, etc.&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E7%BD%91%E7%9B%98dropbox-google-drive-box-onedrive-etc&quot;&gt;网盘：Dropbox, Google Drive, Box, OneDrive, etc.&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#git-%E5%92%8C-github&quot;&gt;Git 和 GitHub&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E6%9E%84%E5%BB%BA-apps-%E7%9A%84%E5%BC%80%E5%8F%91%E8%80%85%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD&quot;&gt;构建 Apps 的开发者基础设施&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#web-app-%E7%98%A6%E5%AE%A2%E6%88%B7%E7%AB%AF&quot;&gt;Web app (瘦客户端)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%B8%A6%E6%9C%AC%E5%9C%B0%E5%AD%98%E5%82%A8%E7%9A%84%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%BA%94%E7%94%A8%E8%83%96%E5%AE%A2%E6%88%B7%E7%AB%AF&quot;&gt;带本地存储的移动端应用（胖客户端）&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%90%8E%E7%AB%AF%E5%8D%B3%E6%9C%8D%E5%8A%A1-baas-firebase-cloudkit-realm&quot;&gt;后端即服务 BaaS: Firebase, CloudKit, Realm&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#couchdb&quot;&gt;CouchDB&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E8%BF%88%E5%90%91%E6%9B%B4%E7%BE%8E%E5%A5%BD%E7%9A%84%E6%9C%AA%E6%9D%A5&quot;&gt;迈向更美好的未来&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#%E4%BB%A5-crdts-%E4%B8%BA%E5%9F%BA%E7%A1%80%E6%80%A7%E6%8A%80%E6%9C%AF&quot;&gt;以 CRDTs 为基础性技术&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#ink--switch-%E5%8E%9F%E5%9E%8B&quot;&gt;Ink \&amp;amp; Switch 原型&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#%E7%9C%8B%E6%9D%BF&quot;&gt;看板&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%8D%8F%E4%BD%9C%E7%BB%98%E7%94%BB&quot;&gt;协作绘画&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%A4%9A%E5%AA%92%E4%BD%93%E7%94%BB%E5%B8%83&quot;&gt;多媒体画布&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%8F%91%E7%8E%B0&quot;&gt;发现&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#crdt-%E6%8A%80%E6%9C%AF%E7%AE%A1%E7%94%A8&quot;&gt;CRDT 技术管用&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E7%A6%BB%E7%BA%BF%E5%B7%A5%E4%BD%9C%E7%9A%84%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C%E9%9D%9E%E5%B8%B8%E5%87%BA%E8%89%B2&quot;&gt;离线工作的用户体验非常出色&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E7%BB%93%E5%90%88-crdt-%E4%B8%8E-functional-reactive-programming-frp-%E5%90%8E%E7%9A%84%E5%BC%80%E5%8F%91%E4%BD%93%E9%AA%8C%E4%B8%8D%E9%94%99&quot;&gt;结合 CRDT 与 Functional Reactive Programming (FRP) 后的开发体验不错&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%86%B2%E7%AA%81%E5%B9%B6%E4%B8%8D%E5%83%8F%E6%88%91%E4%BB%AC%E6%8B%85%E5%BF%83%E7%9A%84%E9%82%A3%E6%A0%B7%E6%98%AF%E4%B8%80%E4%B8%AA%E9%87%8D%E5%A4%A7%E9%97%AE%E9%A2%98&quot;&gt;冲突并不像我们担心的那样是一个重大问题&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%B0%86%E6%96%87%E4%BB%B6%E5%8E%86%E5%8F%B2%E5%8F%AF%E8%A7%86%E5%8C%96%E6%98%AF%E5%BE%88%E9%87%8D%E8%A6%81%E7%9A%84&quot;&gt;将文件历史可视化是很重要的&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#url-%E6%98%AF%E5%BE%88%E5%A5%BD%E7%9A%84%E5%88%86%E4%BA%AB%E6%9C%BA%E5%88%B6&quot;&gt;URL 是很好的分享机制&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#peer-to-peer-%E7%B3%BB%E7%BB%9F%E4%BB%8E%E6%9C%AA%E5%AE%8C%E5%85%A8-%E5%9C%A8%E7%BA%BF-%E6%88%96-%E7%A6%BB%E7%BA%BF%E5%B9%B6%E4%B8%94%E5%BE%88%E9%9A%BE%E6%8E%A8%E6%B5%8B%E6%95%B0%E6%8D%AE%E5%A6%82%E4%BD%95%E6%B5%81%E5%8A%A8&quot;&gt;Peer-to-peer 系统从未完全 “在线” 或 “离线”，并且很难推测数据如何流动&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#crdts-%E7%A7%AF%E7%B4%AF%E4%BA%86%E5%A4%A7%E9%87%8F%E7%9A%84%E5%8F%98%E5%8C%96%E5%8E%86%E5%8F%B2%E4%BA%A7%E7%94%9F%E4%BA%86%E6%80%A7%E8%83%BD%E9%97%AE%E9%A2%98&quot;&gt;CRDTs 积累了大量的变化历史，产生了性能问题&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E4%BB%8D%E7%84%B6%E6%98%AF%E6%9C%AA%E8%A7%A3%E5%86%B3%E7%9A%84%E9%97%AE%E9%A2%98&quot;&gt;网络通信仍然是未解决的问题&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E4%BA%91%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%9C%A8%E5%8F%91%E7%8E%B0%E5%A4%87%E4%BB%BD%E5%92%8C%E7%AA%81%E5%8F%91%E8%AE%A1%E7%AE%97%E6%96%B9%E9%9D%A2%E4%BB%8D%E6%9C%89%E5%85%B6%E5%9C%B0%E4%BD%8D&quot;&gt;云服务器在发现、备份和突发计算方面仍有其地位&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E4%BD%A0%E5%8F%AF%E4%BB%A5%E5%A6%82%E4%BD%95%E6%8F%90%E4%BE%9B%E5%B8%AE%E5%8A%A9&quot;&gt;你可以如何提供帮助&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#%E5%AF%B9%E4%BA%8E%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E5%92%8C%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E7%A0%94%E7%A9%B6%E8%80%85%E6%9D%A5%E8%AF%B4&quot;&gt;对于分布式系统和编程语言研究者来说&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%AF%B9%E4%BA%8E%E4%BA%BA%E6%9C%BA%E4%BA%A4%E4%BA%92hci%E7%A0%94%E7%A9%B6%E8%80%85%E6%9D%A5%E8%AF%B4&quot;&gt;对于人机交互（HCI）研究者来说&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%AF%B9%E4%BA%8E%E4%BB%8E%E4%B8%9A%E8%80%85&quot;&gt;对于从业者&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%AF%B9%E5%88%9D%E5%88%9B%E4%BC%81%E4%B8%9A%E7%9A%84%E5%91%BC%E5%90%81&quot;&gt;对初创企业的呼吁&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E6%80%BB%E7%BB%93&quot;&gt;总结&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E8%87%B4%E8%B0%A2&quot;&gt;致谢&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;动机协作和所属权&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%8A%A8%E6%9C%BA%E5%8D%8F%E4%BD%9C%E5%92%8C%E6%89%80%E5%B1%9E%E6%9D%83&quot; aria-label=&quot;动机协作和所属权 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;动机：协作和所属权&lt;/h1&gt;&lt;p&gt;如今我们可以极为轻松地进行在线协作。我们使用 Google Docs 来协作处理文档、电子表格和演示文稿；在 Figma 中，我们可以多人进行用户界面设计；我们使用 Slack 与同事沟通；我们在 Trello 中跟踪任务。我们依赖这些在线服务来完成笔记、计划项目、保存联系人，以及各类商业用途。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;我们将这些服务称为 “云应用(cloud apps)”，但你也可以称它们为 “SaaS” 或 “基于网络的应用程序”。它们的共同点是，我们通常通过网络浏览器或移动应用程序访问它们，并且它们将数据存储在服务器上。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;与前几代软件相比，今天的云应用提供了很大的好处：无缝协作，并能从任何设备上访问数据。随着我们的生活和工作越来越多地通过这些云应用运行，它们对我们变得越来越重要。我们在使用这些应用程序中投入的时间越多，其中的数据对我们来说就越有价值。&lt;/p&gt;&lt;p&gt;然而通过与很多从事创造性工作的专业人士进行了交流，我们也了解到了云应用的缺点。&lt;/p&gt;&lt;p&gt;当你在制作某样东西时投入了大量的创造性精力和努力，你往往会对它有很深的情感依恋。如果你从事创造性工作，这体会或许不陌生。(当我们说“创造性工作”时，我们指的不仅仅是视觉艺术、音乐、或诗歌—许多其他活动也都是创造性的，如：解释某种技术、实现复杂的算法、设计用户界面、或思考如何带领团队实现目标）。&lt;/p&gt;&lt;p&gt;在进行创造性工作的过程中，你通常会产生文件和数据：文档、演示文稿、电子表格、代码、笔记、图纸等等。你会希望保留这些数据：供将来参考和激发灵感，将其纳入作品集，或者仅仅是存档，因为你为它感到骄傲。这种「拥有」的感受是很重要的，因为创造性的表达是非常个人化的东西。&lt;/p&gt;&lt;p&gt;不幸的是，云应用在这方面是有问题的。虽然他们让你在任何地方访问你的数据，但所有的数据访问都必须通过服务器，而且你只能做服务器让你做的事情。从某种意义上说，你对这些数据没有完全的所有权—而云供应商拥有。所谓云，只是别人的电脑。&lt;/p&gt;&lt;p&gt;There is no cloud, it’s just someone else’s computer.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;我们使用的 “所有权”一词不是指知识产权法和版权，而是指创造者对其数据的感知关系。我们在&lt;a href=&quot;#7-%E4%BD%A0%E4%BF%9D%E7%95%99%E6%9C%80%E7%BB%88%E7%9A%84%E6%89%80%E6%9C%89%E6%9D%83%E5%92%8C%E6%8E%A7%E5%88%B6%E6%9D%83&quot;&gt;后面的章节&lt;/a&gt;中讨论这个概念。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;当数据被存储在 “别人的电脑”上时，它就对该数据有一定的控制能力。云应用是作为一种服务提供的；如果服务不可用，你就不能使用该软件，你也不能再访问你用该软件创建的数据。如果服务关闭，即使你可能能够导出你的数据，但如果没有服务器，你通常没有办法继续运行这个软件。因此，你只能任由提供服务的公司摆布。&lt;/p&gt;&lt;p&gt;在网络应用出现之前，我们有一些我们可以称之为 “老式”的应用：在你的本地计算机上运行的程序，在本地磁盘上读写文件。今天我们仍然在使用这种类型的应用程序：文本编辑器和集成开发环境、Git 和其他版本控制系统，以及许多专业软件，如图形应用程序或 CAD 软件都属于这种类型。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;我们在本文中谈论的软件是用于创建文档或文件（如文本、图形、电子表格、CAD 图纸或音乐），或个人数据存储库（如笔记、日历、待办事项列表或密码管理器）的应用程序。我们不是在谈论实施像银行服务、电子商务、社交网络、共享汽车或类似的服务，这些都是集中式系统能很好提供的。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;在老式的应用程序中，数据存放在你的本地磁盘上的文件中，所以你对这些数据有完全的代理权和所有权：你可以做任何你喜欢的事情，包括长期存档，做备份，使用其他程序操作文件，或在你不再需要它们时删除文件。你不需要任何人的许可来访问你的文件，因为它们是你的。你不必依赖由另一家公司运营的服务器。&lt;/p&gt;&lt;p&gt;总而言之：云计算给了我们协作，但老式的应用程序给了我们所有权。难道我们不能同时拥有这两个世界的优点吗？&lt;/p&gt;&lt;p&gt;我们既希望有云应用提供的方便的跨设备访问和实时协作，也希望有 “老式”软件体现的对你自己数据的拥有权。&lt;/p&gt;&lt;h1 id=&quot;本地优先软件的七个愿景&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E6%9C%AC%E5%9C%B0%E4%BC%98%E5%85%88%E8%BD%AF%E4%BB%B6%E7%9A%84%E4%B8%83%E4%B8%AA%E6%84%BF%E6%99%AF&quot; aria-label=&quot;本地优先软件的七个愿景 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;本地优先软件的七个愿景&lt;/h1&gt;&lt;p&gt;我们相信，数据所有权和实时协作并不相悖。我们有可能创建具有云应用所有优点的软件，同时也允许你保留对你创建的数据、文档和文件的完全所有权。&lt;/p&gt;&lt;p&gt;我们把这种类型的软件称为本地优先软件，因为它优先使用本地存储（你的电脑中内置的磁盘）和本地网络（如你的家庭 WiFi）而不是远程数据中心的服务器。&lt;/p&gt;&lt;p&gt;在云计算应用程序中，服务器上的数据被视为数据的主要的、权威的副本；如果客户端有一份数据的副本，它只是一个从属于服务器的缓冲区。任何数据修改都必须发送到服务器上，否则就 “没有发生”。在本地优先的应用程序中，我们交换了这些角色：我们把你本地设备上的数据副本—你的笔记本电脑、平板电脑或手机—视为主要副本。服务器仍然存在，但它们持有你的数据的次要副本，以协助从多个设备访问。正如我们将看到的，这种观点的变化具有深远的影响。&lt;/p&gt;&lt;p&gt;以下是我们希望在本地优先的软件中实现的七个愿景。&lt;/p&gt;&lt;h2 id=&quot;1-即刻加载数据触手可及&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#1-%E5%8D%B3%E5%88%BB%E5%8A%A0%E8%BD%BD%E6%95%B0%E6%8D%AE%E8%A7%A6%E6%89%8B%E5%8F%AF%E5%8F%8A&quot; aria-label=&quot;1 即刻加载数据触手可及 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 即刻加载：数据触手可及&lt;/h2&gt;&lt;p&gt;今天的许多软件&lt;a href=&quot;https://danluu.com/input-lag/&quot;&gt;感觉比前几代软件慢&lt;/a&gt;。尽管 CPU 的速度越来越快，但在用户的一些输入（如点击一个按钮，或按下一个键）和相应的结果出现在显示屏上之间，往往有可察觉的延迟。在&lt;a href=&quot;https://www.inkandswitch.com/slow-software.html&quot;&gt;此前的工作&lt;/a&gt;中，我们测量了现代软件的性能并分析了这些延迟发生的原因。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/world-ping-times.png&quot;/&gt;
&lt;em&gt;全球不同地点的 AWS 数据中心之间的服务器到服务器的往返时间。 数据来源: Peter Bailis, Aaron Davidson, Alan Fekete, et al.: &lt;a href=&quot;http://arxiv.org/pdf/1302.0309.pdf&quot;&gt;“Highly Available Transactions: Virtues and Limitations,”&lt;/a&gt; VLDB 2014.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;对于云计算应用程序，由于数据的主要副本在服务器上，所有的数据修改和许多数据查询都需要往返于服务器。服务器很可能与你的居住地不在同一片大陆上，所以光速对软件的速度有限制。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;用户界面可能试图通过将操作显示为已经完成来隐藏这一延迟，即使请求仍在进行中—这种模式被称为&lt;a href=&quot;https://uxplanet.org/optimistic-1000-34d9eefe4c05&quot;&gt;乐观的 UI (Optimistic UI)&lt;/a&gt; —但在请求完成之前，总是有可能会失败（例如，由于不稳定的互联网连接）。因此，乐观的 UI 有时仍然会在错误发生时暴露出网络往返的延迟。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;本地优先的软件则不同：因为它把数据的主要副本保存在本地设备上，所以用户永远不需要等待对服务器的请求完成。所有的操作都可以通过读写本地磁盘上的文件来处理，与其他设备的数据同步在后台悄然发生。&lt;/p&gt;&lt;p&gt;虽然这本身并不能保证软件的速度，但我们期望本地优先的软件有可能对用户的输入做出近乎即时的反应，在你等待的时候永远不需要向你展示一个“加载中”的状态，让你的数据触手可及。&lt;/p&gt;&lt;h2 id=&quot;2-多端同步&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#2-%E5%A4%9A%E7%AB%AF%E5%90%8C%E6%AD%A5&quot; aria-label=&quot;2 多端同步 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 多端同步&lt;/h2&gt;&lt;p&gt;如今用户常常使用多种电子设备来完成他们的工作，而现代应用程序必须支持这种工作流程。例如，用户可能会在旅途中使用智能手机捕捉想法，在平板电脑上组织和思考这些想法，然后在笔记本电脑上把结果打成文档。&lt;/p&gt;&lt;p&gt;这意味着，虽然本地优先的应用程序将其数据保存在每个设备的本地存储中，但这些数据也有必要在用户进行工作的所有设备上进行同步。目前存在多种数据同步技术，我们在&lt;a href=&quot;#%E7%8E%B0%E6%9C%89%E7%9A%84%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8%E5%92%8C%E5%85%B1%E4%BA%AB%E6%A8%A1%E5%BC%8F&quot;&gt;后面的章节中&lt;/a&gt;详细讨论。&lt;/p&gt;&lt;p&gt;大多数跨设备同步服务也在服务器上存储数据的副本，这为数据提供了一个方便的异地备份。只要每个文件一次只被一个人编辑，这些解决方案的效果就很好。如果几个人同时编辑同一个文件，可能会产生冲突，这一点我们将在&lt;a href=&quot;#4-%E4%B8%8E%E4%BD%A0%E7%9A%84%E5%90%8C%E4%BA%8B%E6%97%A0%E7%BC%9D%E5%8D%8F%E4%BD%9C&quot;&gt;协作部分&lt;/a&gt;讨论。&lt;/p&gt;&lt;h2 id=&quot;3-网络只是一种可选项&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#3-%E7%BD%91%E7%BB%9C%E5%8F%AA%E6%98%AF%E4%B8%80%E7%A7%8D%E5%8F%AF%E9%80%89%E9%A1%B9&quot; aria-label=&quot;3 网络只是一种可选项 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 网络只是一种可选项&lt;/h2&gt;&lt;p&gt;个人移动设备所处的网络环境经常发生变化：不可靠的咖啡馆 WiFi，在飞机上或火车上穿过隧道时，在电梯或停车场。在发展中国家或农村地区，互联网接入的基础设施有时是分布不均的。在国际旅行时，由于漫游的费用，许多移动用户禁用手机数据。总的来说，有很多对离线应用程序的需求，例如或需要在野外创作的研究人员或者记者。&lt;/p&gt;&lt;p&gt;“老式的”应用程序在没有互联网连接的情况下可以正常工作，但云应用通常在离线时无法工作。几年来，“离线优先”运动一直在鼓励网络和移动应用的开发者改善对离线的支持，但在实践中，很难对云应用的离线支持进行改造，因为为以服务器为中心的模式设计的工具和库不容易适应用户在离线时进行编辑的情况。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;尽管有可能&lt;a href=&quot;https://developers.google.com/web/fundamentals/codelabs/offline/&quot;&gt;使 Web App 在离线状态下工作&lt;/a&gt;，但用户可能很难知道是否 App 所有必要的代码和数据已经下载好了&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;由于本地优先的应用程序将其数据的主要副本存储在每个设备的本地文件系统中，用户即使是在离线时也可以随时读取和写入这些数据。当网络连接可用时，它再与其他设备同步。数据同步不一定要通过互联网：本地优先的应用程序也可以使用蓝牙或本地 WiFi 将数据同步到附近的设备。&lt;/p&gt;&lt;p&gt;此外，为了提供良好的离线支持，软件最好以本地安装的可执行文件的形式在设备上运行，而不是网络浏览器中的一个标签。对于移动应用程序来说，在使用前下载并安装整个应用程序已经是一种标准。&lt;/p&gt;&lt;h2 id=&quot;4-无缝协作&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#4-%E6%97%A0%E7%BC%9D%E5%8D%8F%E4%BD%9C&quot; aria-label=&quot;4 无缝协作 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4. 无缝协作&lt;/h2&gt;&lt;p&gt;协作通常要求多人修改同一份文件或或文档。然而，在老式的软件中，几个人同时在同一份文件上工作常常产生冲突。在代码等文本文件中，解决冲突是枯燥且恼人的，而对于电子表格或图形文件等复杂的文件格式，这项任务很快就变得非常困难甚至不可能完成。因此，协作者可能要事先约定谁来编辑份文件，而且每次只有一个人可以进行修改。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/dropbox-conflict.png&quot;/&gt;
&lt;em&gt;Dropbox 上的冲突编辑，用户需要自行手动合并冲突&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/evernote-conflict.png&quot;/&gt;
&lt;em&gt;Evernote 中，如果笔记被同时修改，那么它就会被移动到冲突修改的笔记中，并且没有支持用户解决冲突的工具&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/merge-conflict.png&quot;/&gt;
&lt;em&gt;在 Git 和其他版本控制系统上，多个人在不通过 commit 上并行修改同一份文件。合并这些改动常常导致“合并冲突”，需要通过专门的工具来解决。这些工具主要是为面向行的文本文件提供的，例如源代码。对于其他类型的文件格式的支持很弱&lt;/em&gt;&lt;/p&gt;&lt;p&gt;另一方面，Google Docs 等云计算应用大大简化了协作，允许多个用户同时编辑一份文件，而不必通过电子邮件来回发送文件，也不必担心冲突。用户已开始期待更多应用支持无缝实时协作。&lt;/p&gt;&lt;p&gt;在本地优先的应用程序中，我们的理想是支持与当今最好的云应用一样的实时协作，甚至更好。实现这一目标是实现本地优先软件的最大挑战之一，但我们相信这是可能的：在&lt;a href=&quot;#%E4%BB%A5-crdts-%E4%B8%BA%E5%9F%BA%E7%A1%80%E6%80%A7%E6%8A%80%E6%9C%AF&quot;&gt;后面的章节&lt;/a&gt;中，我们将讨论在本地优先环境下实现实时协作的技术。&lt;/p&gt;&lt;p&gt;此外，我们期望本地优先的应用程序可以支持各种协作的工作流程。除了让几个人实时协作编辑，有时提供修改建议的模式也是很有价值的。这种模式中由一个人提出修改建议，让其他人审查并有选择性地应用修改。Google Docs 的建议模式和 GitHub 的 Pull Request 就支持这种工作流。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/suggest-changes.png&quot;/&gt;
&lt;em&gt;在 Google Docs 中，协作者可以通过直接编辑文档或者他们也可以提供修改建议，之后可以由文档拥有者来决定是否采用这些建议&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/pull-request.png&quot;/&gt;
&lt;em&gt;GitHub 上的协作工作流程是基于 Pull Request 的。一个用户可以在多个 Commits 中修改多个源文件，并将其作为 Pull Request 提交给一个项目。其他用户可以审查和修改这个 Pull Request&lt;/em&gt;&lt;/p&gt;&lt;h2 id=&quot;5-长期可用&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#5-%E9%95%BF%E6%9C%9F%E5%8F%AF%E7%94%A8&quot; aria-label=&quot;5 长期可用 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5. 长期可用&lt;/h2&gt;&lt;p&gt;数据所有权的一个重要方面是，你可以在未来很长一段时间内继续访问这些数据。当你用本地优先的软件做一些工作时，你的工作应该可以继续无限期地被访问，甚至在生产该软件的公司消失之后。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/cuneiform.jpg&quot;/&gt;
&lt;em&gt;泥板上的楔形文字，约公元前 3000 年。图片来自 &lt;a href=&quot;https://commons.wikimedia.org/wiki/File:Early_writing_tablet_recording_the_allocation_of_beer.jpg&quot;&gt;Wikimedia Commons&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;p&gt;“老式”的应用程序是永久可用的，只要你有对应数据和一些运行软件的方法，即使软件作者破产了，你也可以继续运行该软件。即使操作系统和它所运行的计算机已经过时，你仍然可以在虚拟机或模拟器中运行该软件。存储介质迭代更新了，你也可以把你的文件复制到新的存储介质上，继续访问它们。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;互联网档案馆收集了&lt;a href=&quot;https://archive.org/details/softwarelibrary&quot;&gt;一些历史软件&lt;/a&gt;，可以在现代网络浏览器中使用模拟器运行；&lt;a href=&quot;http://eab.abime.net/&quot;&gt;英国 Amiga 委员会&lt;/a&gt;的爱好者们分享了运行历史软件的技巧。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;另一方面，云应用依赖于服务的可用性：如果服务不可用，你就不能使用该软件，你也不能再访问你用该软件创建的数据。这意味着你在打赌「软件的创建者将长期维护它」—至少在你关心数据的时候。&lt;/p&gt;&lt;p&gt;虽然谷歌似乎没有很快关闭谷歌文档的巨大危险，但&lt;a href=&quot;https://en.wikipedia.org/wiki/Google_Reader&quot;&gt;受欢迎的产品&lt;/a&gt;有时确实会&lt;a href=&quot;https://killedbygoogle.com/&quot;&gt;被关闭&lt;/a&gt;或&lt;a href=&quot;https://www.independent.co.uk/arts-entertainment/music/news/myspace-songs-lost-website-move-migration-mp3-music-server-accounts-a8827881.html&quot;&gt;丢失数据&lt;/a&gt;，所以我们知道要小心。而且，即使是寿命长的软件，也存在价格或功能以你不喜欢的方式改变的风险，而对于云应用，继续使用旧版本不是一种选择—无论你是否喜欢，你都会被升级。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://ourincrediblejourney.tumblr.com/&quot;&gt;我们不可思议的旅程&lt;/a&gt;是一个博客，它记录了创业公司产品在被收购后被关闭的情况。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;本地优先软件有更长的寿命，因为你的数据以及读取和修改你的数据所需的软件，都是在你的电脑上本地存储的。这一点很重要，不仅是为了你自己，也是为了未来的历史学家，他们会想要阅读我们今天创造的文件。如果我们的数据不能长期保存，我们就有可能创造出&lt;a href=&quot;https://www.bbc.co.uk/news/science-environment-31450389&quot;&gt;文特-瑟夫所说的 “数字黑暗时代”&lt;/a&gt;。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;我们以前写过关于&lt;a href=&quot;https://www.inkandswitch.com/capstone-manuscript-appendices.html#c-web-page-archiving&quot;&gt;网页的长期存档的文章&lt;/a&gt;。关于长期数据保存的有趣讨论，请看 Long Tien Nguyen 和 Alan Kay 在 Onward！2015 上发表的论文 &lt;a href=&quot;http://www.vpri.org/pdf/tr2015004_cuneiform.pdf&quot;&gt;《2015 年的楔形文字》&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;一些文件格式（如纯文本、JPEG 和 PDF）是如此的普遍，以至于它们可能在未来几个世纪内都可以被阅读。美国国会图书馆也&lt;a href=&quot;https://www.sqlite.org/locrsf.html&quot;&gt;推荐 XML、JSON 或 SQLite&lt;/a&gt; 作为数据集的存档格式。然而，为了读取不太常见的文件格式并保留交互，你需要能够运行原始软件（如果有必要，在虚拟机或模拟器中）。本地优先软件可以实现这一点。&lt;/p&gt;&lt;h2 id=&quot;6-安全性和隐私性&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#6-%E5%AE%89%E5%85%A8%E6%80%A7%E5%92%8C%E9%9A%90%E7%A7%81%E6%80%A7&quot; aria-label=&quot;6 安全性和隐私性 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;6. 安全性和隐私性&lt;/h2&gt;&lt;p&gt;云应用架构的一个问题是，它们将所有用户的所有数据存储在一个集中的数据库中。这种大量的数据收集对攻击者来说是充满吸引力的目标：一个&lt;a href=&quot;https://www.bloomberg.com/news/articles/2019-04-10/is-anyone-listening-to-you-on-alexa-a-global-team-reviews-audio&quot;&gt;流氓雇员&lt;/a&gt;，或一个获得公司服务器访问权的黑客，可以阅读和篡改你的所有数据。令人恐惧的是&lt;a href=&quot;https://en.wikipedia.org/wiki/List_of_data_breaches&quot;&gt;这样的安全漏洞很常见&lt;/a&gt;。我们不幸地任由云应用供应商的摆布。&lt;/p&gt;&lt;p&gt;虽然谷歌有世界级的安全团队，但可悲的现实是，大多数公司都没有。虽然谷歌善于保护你的数据免受外部攻击，但公司内部却可以自由地以各种方式使用你的数据，例如将你的数据输入其机器学习系统。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;引用 Google Drive 服务条款的内容。“我们的自动化系统分析你的内容，为你提供个人相关的产品功能，如定制的搜索结果，以及垃圾邮件和恶意软件检测。”&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;也许你觉得你的数据不会引起任何攻击者的兴趣。然而，对于许多职业来说，处理敏感数据是他们工作的重要部分。例如，医疗专业人士处理敏感的病人数据，调查记者处理消息来源的机密信息，政府和外交代表进行敏感的谈判，等等。由于法规遵从和保密义务，这些专业人士中的许多人不能使用云应用。&lt;/p&gt;&lt;p&gt;另一方面，本地优先的应用程序，在核心部分内置了更好的隐私和安全。你的本地设备只存储你自己的数据，避免了集中式的云数据库保存着所有人的数据。本地优先的应用程序可以使用端对端加密，这样，任何存储你的文件副本的服务器都只持有他们无法读取的加密数据。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;像 &lt;a href=&quot;https://www.usenix.org/conference/usenixsecurity16/technical-sessions/presentation/garman&quot;&gt;iMessage&lt;/a&gt;、&lt;a href=&quot;https://www.whatsapp.com/security/WhatsApp-Security-Whitepaper.pdf&quot;&gt;WhatsApp&lt;/a&gt; 和 &lt;a href=&quot;https://www.signal.org/docs/&quot;&gt;Signal&lt;/a&gt; 这样的现代信息应用已经使用了端对端加密，&lt;a href=&quot;https://keybase.io/&quot;&gt;Keybase&lt;/a&gt; 提供了加密的文件共享和信息传递，&lt;a href=&quot;https://www.tarsnap.com/crypto.html&quot;&gt;Tarsnap&lt;/a&gt; 也采取了这种方法进行备份。我们希望看到这种趋势也能扩展到其他类型的软件。&lt;/p&gt;&lt;/blockquote&gt;&lt;h2 id=&quot;7-你保留最终的所有权和控制权&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#7-%E4%BD%A0%E4%BF%9D%E7%95%99%E6%9C%80%E7%BB%88%E7%9A%84%E6%89%80%E6%9C%89%E6%9D%83%E5%92%8C%E6%8E%A7%E5%88%B6%E6%9D%83&quot; aria-label=&quot;7 你保留最终的所有权和控制权 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;7. 你保留最终的所有权和控制权&lt;/h2&gt;&lt;p&gt;对于云应用，服务提供商有权力限制用户的访问：在 2017 年 10 月，几个谷歌文档的用户被锁定在他们的文件之外，因为某个自动系统错误地将这些文件标记为违规。在本地优先的应用程序中，数据的所有权归属于用户。&lt;/p&gt;&lt;p&gt;在本文中提及的“所有权”并不是指知识产权的法律意义上的所有权。例如，文字处理工具无法知晓谁拥有所编辑文本的版权。而我们指的是对数据的控制的意义上的所有权：你应该能够以任何方式复制和修改数据，写下任何想法，没有公司应该限制你的行为。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;根据&lt;a href=&quot;https://www.echr.coe.int/Documents/Convention_ENG.pdf&quot;&gt;《欧洲人权公约》&lt;/a&gt;，你的思想和意见自由是&lt;a href=&quot;https://www.echr.coe.int/Documents/Guide_Art_9_ENG.pdf&quot;&gt;无条件的&lt;/a&gt;—国家永远不得干涉，因为它只属于你自己—而言论自由（包括言论自由）则可以在某些方面受到限制，因为它影响到其他人。像社交网络这样的通信服务传达的是表达，但创作人的原始笔记和未发表的作品是发展思想和意见的一种方式，因此值得&lt;a href=&quot;http://data.parliament.uk/writtenevidence/committeeevidence.svc/evidencedocument/draft-investigatory-powers-bill-committee/draft-investigatory-powers-bill/written/26353.pdf&quot;&gt;无条件地保护&lt;/a&gt;。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;在云应用中，访问和修改你自己的数据的方式受到服务提供商的 API、用户界面和服务条款的限制。在本地优先的软件中，构成你的数据的所有字节都存储在你自己的设备上，所以你可以自由地以任意的方式处理这些数据。&lt;/p&gt;&lt;p&gt;伴随数据所有权而来的是相对应的责任：采用备份或者其他防止数据丢失的措施，防止勒索软件，以及组织和管理文件。对专业用户来说，我们认为用更多的责任来换取更多的所有权是可取的(如&lt;a href=&quot;#%E5%8A%A8%E6%9C%BA%E5%8D%8F%E4%BD%9C%E5%92%8C%E6%89%80%E5%B1%9E%E6%9D%83&quot;&gt;本文开头&lt;/a&gt;中所介绍的)。例如重要的个人创作，如博士论文或电影的原始镜头。对于这些数据，你可能愿意承担存储和备份的责任，以确保你的数据是安全的，完全在你的控制之下。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;在我们看来，保持对数据的控制和所有权并不意味着软件必须是开放源代码的。尽管修改软件的自由有价值，但只要不人为地限制用户对其文件的操作，商业和闭源软件是可以满足本地优先的理想的。这种人为限制的例子有：禁用打印等操作的 PDF 文件，干扰复制粘贴的电子书阅读器，以及媒体文件的 DRM。&lt;/p&gt;&lt;/blockquote&gt;&lt;h1 id=&quot;现有的数据存储和共享模式&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E7%8E%B0%E6%9C%89%E7%9A%84%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8%E5%92%8C%E5%85%B1%E4%BA%AB%E6%A8%A1%E5%BC%8F&quot; aria-label=&quot;现有的数据存储和共享模式 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;现有的数据存储和共享模式&lt;/h1&gt;&lt;p&gt;我们相信专业和创造性的用户应该得到能够实现本地优先目标的软件，帮助他们进行无缝协作，同时也允许他们保留对工作成果的完全所有权。如果我们能在用户用来做最重要工作的软件中提供这些品质，我们就能帮助他们更好地完成工作，并有可能为许多人的职业生活带来重大改变。&lt;/p&gt;&lt;p&gt;然而，虽然本地优先软件的理想可能会引起你的共鸣，但你可能仍想知道它们的可行性。它们是否只是乌托邦式的幻想？&lt;/p&gt;&lt;p&gt;在下文中，我们将讨论在实践中实现本地优先的软件意味着什么。我们考察了大量的现有技术，并对它们满足本地优先理想的程度进行了分解。在下面的表格中，✓ 表示技术符合理想，-表示部分符合理想，而 ✗ 表示不符合理想。&lt;/p&gt;&lt;p&gt;正如我们将看到的，许多技术满足了部分目标，但没有一项技术能够满足所有目标。最后，我们将研究一种来自计算机科学研究前沿的技术，它可能是未来实现本地优先软件的重要基础设施。&lt;/p&gt;&lt;h2 id=&quot;应用程序架构如何影响用户体验&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F%E6%9E%B6%E6%9E%84%E5%A6%82%E4%BD%95%E5%BD%B1%E5%93%8D%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C&quot; aria-label=&quot;应用程序架构如何影响用户体验 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;应用程序架构如何影响用户体验&lt;/h2&gt;&lt;p&gt;让我们先从最终用户的角度来审视软件，并分解不同的软件架构在多大程度上满足了本地优先软件的&lt;a href=&quot;#%E6%9C%AC%E5%9C%B0%E4%BC%98%E5%85%88%E8%BD%AF%E4%BB%B6%E7%9A%84%E4%B8%83%E4%B8%AA%E6%84%BF%E6%99%AF&quot;&gt;七个目标&lt;/a&gt;。在下一节中，我们将比较软件工程师用来构建应用程序的存储技术和 API。&lt;/p&gt;&lt;h3 id=&quot;文件和邮件附件&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E6%96%87%E4%BB%B6%E5%92%8C%E9%82%AE%E4%BB%B6%E9%99%84%E4%BB%B6&quot; aria-label=&quot;文件和邮件附件 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;文件和邮件附件&lt;/h3&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;1. 快&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;2. 多设备&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;3. 离线&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;4. 协作&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;5. 长期可用性&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;6. 隐私&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;7. 控制权&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;文件和邮件附件&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;从我们的七个目标来看，传统的文件有许多理想的特性：它们可以被离线查看和编辑，它们给了用户完全的控制权，它们可以随时被备份和长期保存。依靠本地文件的软件也有可能变得非常快。&lt;/p&gt;&lt;p&gt;然而，从多个设备访问文件是比较棘手的。有多种方式在设备间传输文件：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;通过电子邮件来回发送&lt;/li&gt;&lt;li&gt;来回传递 U 盘&lt;/li&gt;&lt;li&gt;通过分布式文件系统，如 NAS 服务器、NFS、FTP 或 rsync&lt;/li&gt;&lt;li&gt;使用云文件存储服务，如 Dropbox、Google Drive 或 OneDrive（见后面章节）&lt;/li&gt;&lt;li&gt;使用版本控制系统，如 Git（见后面的章节）&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;其中，电子邮件附件可能是最常见的共享机制，特别是在那些不是技术专家的用户中。附件很容易理解和信任。一旦你有了一份文件的副本，除非用户主动修改，否则它就不会变：如果你在六个月后查看一封电子邮件，附件仍然以其原始形式存在。与网络应用不同，附件不需要任何额外的登录过程就可以打开。&lt;/p&gt;&lt;p&gt;电子邮件附件最薄弱的地方是协作。一般来说，一次只能有一个人对文件进行修改，否则就需要进行困难的手动合并。文件的版本很快就会变得混乱，往返发送的协作文件常导致文件名变成奇怪的样子： &lt;code class=&quot;language-text&quot;&gt;预算草案 （小明版）绝对最终版（2）.xls&lt;/code&gt;。&lt;/p&gt;&lt;p&gt;尽管如此，对于那些想要融入本地优先理念的应用程序来说，一个好的起点是提供导出功能，生成被广泛支持的文件格式（如纯文本、PDF、PNG 或 JPEG），并允许它被分享，例如通过电子邮件附件、Slack 或 WhatsApp。&lt;/p&gt;&lt;h3 id=&quot;网页应用google-docs-trello-figma-pinterest-etc&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E7%BD%91%E9%A1%B5%E5%BA%94%E7%94%A8google-docs-trello-figma-pinterest-etc&quot; aria-label=&quot;网页应用google docs trello figma pinterest etc permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;网页应用：Google Docs, Trello, Figma, Pinterest, etc.&lt;/h3&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;1. 快&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;2. 多设备&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;3. 离线&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;4. 协作&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;5. 长期可用性&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;6. 隐私&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;7. 控制权&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Google Docs&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Trello&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Pinterest&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;在光谱的另一端是纯 Web App，此时用户的本地软件（浏览器或移动应用）是瘦客户端，数据存储在服务器上。服务器通常使用一个大规模的数据库，其中数百万用户的数据都混合在一个巨大的集合中。&lt;/p&gt;&lt;p&gt;Web 应用程序已经设定了实时协作的标准。作为用户，你可以相信，当你在任何设备上打开文件时，你看到的是最新的内容，用的是最新的版本。这对团队工作来说是非常有用的，以至于这些应用程序已经成为主导。甚至像微软 Office 这样传统的纯本地软件也在向云服务过渡，&lt;a href=&quot;http://fortune.com/2017/07/20/microsoft-office-365-earnings/&quot;&gt;截至 2017 年，Office 365 已经让本地安装的 Office 黯然失色&lt;/a&gt;。&lt;/p&gt;&lt;p&gt;随着&lt;a href=&quot;https://medium.com/@anupamr/distributed-teams-are-the-new-cloud-for-startups-14240a9822d7&quot;&gt;远程工作和分布式团队的兴起&lt;/a&gt;，实时协作的生产力工具正变得更加重要。团队视频通话中的十个用户可以调出同一个 Trello 板，每个人在自己的电脑上进行编辑，同时看到其他用户正在做什么。&lt;/p&gt;&lt;p&gt;这样做的负面是所有权和控制权的完全丧失：服务器上的数据才是最重要的，而你的客户端设备上的任何数据都是不重要的，它只是缓存。大多数网络应用程序对离线支持很差或者根本不支持：如果你的网络出现哪怕是片刻的故障，你的工作就会被锁定。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/gdocs-offline.png&quot;/&gt;
&lt;em&gt;如果 Google Docs 检测到它是离线的，它就禁止文档编辑&lt;/em&gt;&lt;/p&gt;&lt;p&gt;一些最好的网络应用程序使用 JavaScript &lt;a href=&quot;https://uxplanet.org/optimistic-1000-34d9eefe4c05&quot;&gt;隐藏了服务器通信的延迟&lt;/a&gt;，并试图提供有限的离线支持（例如，&lt;a href=&quot;https://chrome.google.com/webstore/detail/google-docs-offline/ghbmnnjooekpmoecnnnilnnbdlolhkhi&quot;&gt;Google Docs 的离线插件&lt;/a&gt;）。然而，这些努力似乎都是为了适应从根本上以与服务器同步互动为中心的应用架构。用户在尝试离线工作时报告了不同的结果。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/gdocs-extension.png&quot;/&gt;
&lt;em&gt;Google Docs 离线插件的负面评价&lt;/em&gt;&lt;/p&gt;&lt;p&gt;一些网络应用，例如 Milanote 和 Figma，提供了可安装的桌面客户端，基本上是重新包装的网络浏览器。如果你试图使用这些客户端访问你的工作，而你的网络是断断续续的，当供应商的服务器遇到故障时，或者在供应商被收购并关闭后，很明显，你的工作从来就不是真正属于你的。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/figma-offline.png&quot;/&gt;
&lt;em&gt;Figma 桌面端离线时&lt;/em&gt;&lt;/p&gt;&lt;h3 id=&quot;网盘dropbox-google-drive-box-onedrive-etc&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E7%BD%91%E7%9B%98dropbox-google-drive-box-onedrive-etc&quot; aria-label=&quot;网盘dropbox google drive box onedrive etc permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;网盘：Dropbox, Google Drive, Box, OneDrive, etc.&lt;/h3&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;1. 快&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;2. 多设备&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;3. 离线&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;4. 协作&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;5. 长期可用性&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;6. 隐私&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;7. 控制权&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Dropbox&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;基于云的文件同步产品，如 Dropbox、Google Drive、Box 或 OneDrive，使文件在多个设备上可用。在桌面操作系统（Windows、Linux、Mac OS）上，这些工具通过观察本地文件系统上的指定文件夹来工作。你的电脑上的任何软件都可以读写这个文件夹中的文件，每当一台电脑上的文件被改变，它就会自动复制到你所有的其他电脑上。&lt;/p&gt;&lt;p&gt;由于这些工具使用本地文件系统，它们有许多吸引人的特性：对本地文件的访问是快速的，离线工作也没有问题（离线编辑的文件在下次有互联网连接的时候会被同步）。如果同步服务被关闭，你的文件仍然会在你的本地磁盘上保持无损，而且很容易切换到另一个同步服务。如果你的电脑硬盘出现故障，你只需安装应用程序并等待它同步，就可以恢复你的工作。这提供了良好的长期可用性和对数据的控制能力。&lt;/p&gt;&lt;p&gt;然而，在移动平台（iOS 和安卓）上，Dropbox 及其表亲使用了完全不同的模式。移动应用程序不同步整个文件夹—相反，它们是瘦客户端，每次从服务器上获取你的数据一个文件，而且默认情况下，它们不能脱机工作。有个 &lt;a href=&quot;https://help.dropbox.com/mobile/access-files-offline&quot;&gt;“离线可用”&lt;/a&gt;的选项，但你需要记住在离线之前调用它，它是笨拙的，而且只有在应用程序打开时才有效。&lt;a href=&quot;https://www.dropbox.com/developers&quot;&gt;Dropbox 的 API&lt;/a&gt; 也是非常以服务器为中心的。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/dropbox-mobile.jpg&quot;/&gt;
&lt;em&gt;Dropbox 移动端的用户的很多时间花在“加载中”的页面上，和桌面端即刻加载的体验大相径庭&lt;/em&gt;&lt;/p&gt;&lt;p&gt;文件同步产品的最弱点是缺乏实时协作：如果同一个文件在两个不同的设备上被编辑，其结果是需要手动合并的冲突，如前所述。这些工具可以同步任何格式的文件，这既是一个优点（与任何应用程序兼容），也是一个缺点（无法进行特定格式的合并）。&lt;/p&gt;&lt;h3 id=&quot;git-和-github&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#git-%E5%92%8C-github&quot; aria-label=&quot;git 和 github permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Git 和 GitHub&lt;/h3&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;1. 快&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;2. 多设备&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;3. 离线&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;4. 协作&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;5. 长期可用性&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;6. 隐私&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;7. 控制权&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Git+GitHub&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;a href=&quot;https://git-scm.com/&quot;&gt;Git&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt; 主要被软件工程师用来进行源代码的协作。它们也许是我们最接近真正的本地优先的软件包：与以服务器为中心的版本控制系统（如 [Subversion](&lt;a href=&quot;https://subversion.apache.org/%EF%BC%89%E7%9B%B8%E6%AF%94%EF%BC%8CGit&quot;&gt;https://subversion.apache.org/）相比，Git&lt;/a&gt; 可以完全脱机工作，它速度快，可以给用户完全的控制权，而且适合长期保存数据。之所以如此，是因为本地文件系统上的 Git 仓库是数据的主要副本，不从属于任何服务器。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;我们在此着重介绍 Git/GitHub，作为最成功的例子，但这些经验也适用于其他分布式版本控制器，如 Mercurial 或 Darcs，以及其他仓库托管服务，如 GitLab 或 Bitbucket。原则上，没有仓库服务也可以进行协作，例如&lt;a href=&quot;https://drewdevault.com/2018/07/02/Email-driven-git.html&quot;&gt;通过电子邮件发送补丁文件&lt;/a&gt;，但大多数 Git 用户都依赖 GitHub。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;像 GitHub 这样的仓库托管服务可以实现围绕 Git 仓库的协作，从多个设备上访问数据，并提供一个备份和存档位置。目前对移动设备的支持还很薄弱，尽管 &lt;a href=&quot;https://workingcopyapp.com/&quot;&gt;Working Copy&lt;/a&gt; 是一个很有前途的 iOS 版 Git 客户端。GitHub 存储的仓库是不加密的；如果需要更强的隐私保护，你可以运行自己的仓库服务器。&lt;/p&gt;&lt;p&gt;我们认为 Git 的模式为本地优先的软件指明了方向。然而，就目前而言，Git 有两个主要的弱点。&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Git 在异步协作方面非常出色，尤其是使用 &lt;a href=&quot;https://help.github.com/en/articles/about-pull-requests&quot;&gt;Pull Requests&lt;/a&gt;，它可以获取一组粗略的修改，并在合并到共享的主干分支之前对其进行讨论和修改。但 Git 并不具备实时、细粒度协作的能力，比如 Google Docs、Trello 和 Figma 等工具中出现的自动、即时合并。&lt;/li&gt;&lt;li&gt;Git 对代码和类似的基于行的文本文件进行了高度优化；其他文件格式则被视为二进制文件，无法进行有意义的编辑或合并。尽管 GitHub 努力显示和比较图像、散文和 CAD 文件，但非文本文件格式在 Git 中仍然是次要的。&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;有趣的是，大多数软件工程师都不愿意接受云版本的编辑器、IDE、运行时环境和构建工具。从理论上讲，我们一般会预期这个用户群体比其他类型的用户更早地接受新技术。但是，如果你问工程师为什么不使用 &lt;a href=&quot;https://aws.amazon.com/cloud9/&quot;&gt;Cloud9&lt;/a&gt; 或 &lt;a href=&quot;https://repl.it/&quot;&gt;Repl.it&lt;/a&gt; 这样的云端编辑器，或 &lt;a href=&quot;https://colab.research.google.com/&quot;&gt;Colaboratory&lt;/a&gt; 这样的运行环境，答案通常包括“它太慢了”或“我不信任它”或“我希望我的代码在我的本地系统”。这些情绪似乎反映了一些与本地优先的软件相同的动机。如果我们作为开发者为自己和我们的工作想要这些东西，也许我们可以想象，其他类型的创造性的专业人士也会为他们自己的工作想要这些相同的特性。&lt;/p&gt;&lt;h2 id=&quot;构建-apps-的开发者基础设施&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E6%9E%84%E5%BB%BA-apps-%E7%9A%84%E5%BC%80%E5%8F%91%E8%80%85%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD&quot; aria-label=&quot;构建 apps 的开发者基础设施 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;构建 Apps 的开发者基础设施&lt;/h2&gt;&lt;p&gt;现在我们已经通过本地优先的理念考察了一系列应用程序的用户体验，让我们把思维转换到应用程序开发者的角度。如果你正在创建应用程序，并希望为用户提供部分或全部本地优先的体验，你在数据存储和同步基础设施方面有哪些选择？&lt;/p&gt;&lt;h3 id=&quot;web-app-瘦客户端&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#web-app-%E7%98%A6%E5%AE%A2%E6%88%B7%E7%AB%AF&quot; aria-label=&quot;web app 瘦客户端 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Web app (瘦客户端)&lt;/h3&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;1. 快&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;2. 多设备&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;3. 离线&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;4. 协作&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;5. 长期可用性&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;6. 隐私&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;7. 控制权&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Web apps&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;最纯粹的网络应用通常是在服务器上运行的 Rails、Django、PHP 或 Node.js 程序，将其数据存储在 SQL 或 NoSQL 数据库中，并通过 HTTPS 提供网页。所有的数据都在服务器上，而用户的网络浏览器只是一个瘦客户端。&lt;/p&gt;&lt;p&gt;这种架构有很多好处：零安装（只需访问一个 URL），用户无需管理，因为所有数据都由部署应用程序的工程和 DevOps 专业人员存储和管理在同一个地方。用户可以从他们所有的设备上访问该应用程序，同事们可以通过登录同一个应用程序轻松地进行合作。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Meteor 和 ShareDB 等 JavaScript 框架，以及 Pusher 和 Ably 等服务，使得在 WebSocket 等低级协议的基础上，更容易向 Web 应用添加实时协作功能。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;另一方面，一个网络应用如果需要为每个用户的行动向服务器执行请求，那么速度就会很慢。在某些情况下，可以通过使用客户端的 JavaScript 来隐藏通讯时间，但如果用户的互联网连接不稳定，这些方法很快就会失效。&lt;/p&gt;&lt;p&gt;尽管有许多努力使网络浏览器对离线更友好（&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache&quot;&gt;minifests&lt;/a&gt;、&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage&quot;&gt;localStorage&lt;/a&gt;、&lt;a href=&quot;https://developers.google.com/web/fundamentals/primers/service-workers/&quot;&gt;service workers&lt;/a&gt; 和 &lt;a href=&quot;https://developers.google.com/web/progressive-web-apps/&quot;&gt;Progressive Web Apps&lt;/a&gt; 等），但网络应用程序的架构从根本上说仍然是以服务器为中心的。离线支持在大多数网络应用中都是后续增加的能力，因而往往很脆弱。在许多网络浏览器中，如果用户清除了他们的 cookies，&lt;a href=&quot;https://stackoverflow.com/questions/8537112/when-is-localstorage-cleared&quot;&gt;本地存储中的所有数据也会被删除&lt;/a&gt;；虽然这对缓存来说不是一个问题，但它使得浏览器的本地存储不适合存储任何长期的重要数据。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;新闻网站&lt;a href=&quot;https://www.theguardian.com/info/developer-blog/2015/nov/04/building-an-offline-page-for-theguardiancom&quot;&gt;《卫报》&lt;/a&gt;记录了他们如何使用 Service Worker 为用户建立离线体验。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;依靠第三方网络应用在长期可用性、隐私和用户控制方面的得分也很低。如果网络应用是开源的，并且用户愿意自我托管他们自己的服务器实例，就有可能改善这些属性。然而，我们认为对于绝大多数用户来说自我托管&lt;a href=&quot;https://rosenzweig.io/blog/the-federation-fallacy.html&quot;&gt;并不是一个可行的选择&lt;/a&gt;；此外，大多数网络应用是闭源的，完全排除了这种选择。&lt;/p&gt;&lt;p&gt;总而言之，我们推测，由于平台的瘦客户端性质，网络应用将永远无法提供我们所寻找的所有本地优先的属性。选择建立一个 Web App，你就选择了属于你和你的公司，而不是属于你的用户的数据之路。&lt;/p&gt;&lt;h3 id=&quot;带本地存储的移动端应用胖客户端&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%B8%A6%E6%9C%AC%E5%9C%B0%E5%AD%98%E5%82%A8%E7%9A%84%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%BA%94%E7%94%A8%E8%83%96%E5%AE%A2%E6%88%B7%E7%AB%AF&quot; aria-label=&quot;带本地存储的移动端应用胖客户端 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;带本地存储的移动端应用（胖客户端）&lt;/h3&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;1. 快&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;2. 多设备&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;3. 离线&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;4. 协作&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;5. 长期可用性&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;6. 隐私&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;7. 控制权&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Thick client&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;iOS 和安卓的应用程序是本地安装的软件，在运行应用程序之前下载并安装整个应用程序二进制。然而，许多应用程序是瘦客户端，类似于 Web 应用程序，需要服务器才能运行（例如，Twitter、Yelp 或 Facebook）。如果没有可靠的互联网连接，这些应用程序会给你带来“加载中”、“出错了”和意外行为。&lt;/p&gt;&lt;p&gt;然而，还有一类移动应用程序更符合本地优先的理想。这些应用首先在本地设备上存储数据，使用 SQLite、Core Data 等持久层，或者只是普通文件。其中一些（如 &lt;a href=&quot;https://helloclue.com/&quot;&gt;Clue&lt;/a&gt; 或 &lt;a href=&quot;https://culturedcode.com/things/&quot;&gt;Things&lt;/a&gt;）开始时是一个没有任何服务器的单用户应用程序，后来增加了云后端，作为一种在设备间同步或与其他用户共享数据的方式。&lt;/p&gt;&lt;p&gt;这些胖客户端应用程序的优点是快速和离线工作，因为服务器同步发生在后台。如果服务器被关闭，它们通常会继续工作。它们提供隐私和用户对数据控制的程度取决于相关的应用程序。&lt;/p&gt;&lt;p&gt;如果数据可能在多个设备上被修改或被多个协作用户修改，事情就会变得更加困难。移动应用的开发者通常是终端用户应用开发方面的专家，而不是分布式系统方面的专家。我们已经看到多个应用开发团队在编写他们自己的临时差异、合并和冲突解决算法，而由此产生的数据同步解决方案往往是不可靠的和脆弱的。下一节中讨论的更专业的存储后端可以提供帮助。&lt;/p&gt;&lt;h3 id=&quot;后端即服务-baas-firebase-cloudkit-realm&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%90%8E%E7%AB%AF%E5%8D%B3%E6%9C%8D%E5%8A%A1-baas-firebase-cloudkit-realm&quot; aria-label=&quot;后端即服务 baas firebase cloudkit realm permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;后端即服务 BaaS: Firebase, CloudKit, Realm&lt;/h3&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;1. 快&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;2. 多设备&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;3. 离线&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;4. 协作&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;5. 长期可用性&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;6. 隐私&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;7. 控制权&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Firebase, CloudKit, Realm&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;a href=&quot;https://firebase.google.com/&quot;&gt;Firebase&lt;/a&gt; 是最成功的移动后盾即服务选项。它本质上是一个本地设备上的数据库与云数据库服务和两者之间的数据同步相结合。Firebase 允许在多个设备上共享数据，并支持&lt;a href=&quot;https://firebase.google.com/docs/firestore/manage-data/enable-offline&quot;&gt;离线使用&lt;/a&gt;。然而，作为一个专有的托管服务，我们在隐私和长期可用性上给它打了低分。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;另一个受欢迎的后台即服务是 &lt;a href=&quot;https://en.wikipedia.org/wiki/Parse_(platform)&quot;&gt;Parse&lt;/a&gt;，但它在 2017 年被 Facebook 收购，然后&lt;a href=&quot;https://willowtreeapps.com/ideas/parse-shutdown-what-it-means-and-what-you-can-do&quot;&gt;被关闭&lt;/a&gt;。依赖于它的应用程序被迫转移到其他后台服务，这强调了长期可用性的重要性。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Firebase 开发者提供了很好的体验：你可以在 Firebase 控制台以自由的方式查看、编辑和删除数据。但是，用户却没有类似的方式来访问、操作和管理他们的数据，使得用户几乎没有所有权和控制权。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/firebase-console.png&quot;/&gt;
&lt;em&gt;Firebase 控制台: 对开发者非常友好，终端用户则无法使用&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://developer.apple.com/icloud/cloudkit/&quot;&gt;苹果的 CloudKit&lt;/a&gt; 为愿意将自己限制在 iOS 和 Mac 平台上的应用提供了类似 Firebase 的体验。它是一个具有同步功能的键值存储，具有良好的离线能力，而且它还有一个额外的好处，那就是内置于平台中（从而避免了用户必须创建一个账户并登录的笨拙）。对于独立的 iOS 开发者来说，这是一个很好的选择，像 &lt;a href=&quot;https://ulysses.app/&quot;&gt;Ulysses&lt;/a&gt;、&lt;a href=&quot;https://bear.app/&quot;&gt;Bear&lt;/a&gt;、&lt;a href=&quot;https://overcast.fm/&quot;&gt;Overcast&lt;/a&gt; 等工具都使用了它，效果不错。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/ulysses-sync.png&quot;/&gt;
&lt;em&gt;通过一个复选框，Ulysses在用户的所有连接设备上同步工作，这得益于它对CloudKit的使用&lt;/em&gt;&lt;/p&gt;&lt;p&gt;这方面的另一个项目是 &lt;a href=&quot;https://realm.io/&quot;&gt;Realm&lt;/a&gt;。与 Core Data 相比，这个用于 iOS 的持久化库因其更简洁的 API 而获得欢迎。用于本地持久化的客户端库被称为 Realm 数据库，而相关的类似 Firebase 的后端服务被称为 &lt;a href=&quot;https://docs.realm.io/sync/what-is-realm-platform&quot;&gt;Realm 对象服务器&lt;/a&gt;。值得注意的是，对象服务器是&lt;a href=&quot;https://github.com/realm/realm-object-server&quot;&gt;开源的，可自行托管&lt;/a&gt;，这减少了被锁定在一个可能有一天会消失的服务上的风险。&lt;/p&gt;&lt;p&gt;移动应用程序将设备上的数据作为主要副本（或至少比一次性缓存多），并使用 Firebase 或 iCloud 等同步服务，使我们在实现本地优先软件上有了很大的进展。&lt;/p&gt;&lt;h3 id=&quot;couchdb&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#couchdb&quot; aria-label=&quot;couchdb permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CouchDB&lt;/h3&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;1. 快&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;2. 多设备&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;3. 离线&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;4. 协作&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;5. 长期可用性&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;6. 隐私&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;7. 控制权&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;CouchDB&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;a href=&quot;https://couchdb.apache.org/&quot;&gt;CouchDB&lt;/a&gt; 是一个数据库，因开创了多主机复制而备受关注：每台机器都有一个成熟的数据库副本，每个副本都可以独立地对数据进行修改，任何一对副本都可以相互同步，以交换最新的修改。CouchDB 是为在服务器上使用而设计的；&lt;a href=&quot;https://www.ibm.com/cloud/cloudant&quot;&gt;Cloudant&lt;/a&gt; 提供了一个托管版本；&lt;a href=&quot;https://pouchdb.com/&quot;&gt;PouchDB&lt;/a&gt; 和 &lt;a href=&quot;http://hood.ie/&quot;&gt;Hoodie&lt;/a&gt; 是兄弟项目，使用相同的同步协议，但被设计为在终端用户设备上运行。&lt;/p&gt;&lt;p&gt;从哲学上讲，CouchDB 与本地优先原则密切相关，这一点在 &lt;a href=&quot;http://guide.couchdb.org/&quot;&gt;CouchDB 的书&lt;/a&gt;中尤其明显，该书对&lt;a href=&quot;http://guide.couchdb.org/editions/1/en/consistency.html&quot;&gt;分布式一致性&lt;/a&gt;、&lt;a href=&quot;http://guide.couchdb.org/editions/1/en/replication.html&quot;&gt;复制&lt;/a&gt;、&lt;a href=&quot;http://guide.couchdb.org/editions/1/en/notifications.html&quot;&gt;变更通知&lt;/a&gt;和&lt;a href=&quot;https://en.wikipedia.org/wiki/Multiversion_concurrency_control&quot;&gt;多版本并发控制&lt;/a&gt;等相关主题做了很好的介绍。&lt;/p&gt;&lt;p&gt;虽然 CouchDB/PouchDB 允许多个设备同时对数据库进行修改，但这些修改会导致冲突，需要由应用程序代码显式解决。这种冲突解决代码很难正确编写，这使得 CouchDB 对于具有非常精细的协作的应用来说是不切实际的，比如在 Google Docs 中，每个按键都有可能是一个单独的修改。&lt;/p&gt;&lt;p&gt;在实践中，CouchDB 模型&lt;a href=&quot;https://medium.com/offline-camp/couchdb-pouchdb-and-hoodie-as-a-stack-for-progressive-web-apps-a6078a985f18&quot;&gt;还没有被广泛采用&lt;/a&gt;。这其中有各种原因：当每个用户需要一个单独的数据库时，存在可扩展性问题；在 iOS 和 Android 的本地应用程序中嵌入 JavaScript 客户端存在困难；冲突解决的问题；使用不常见的 MapReduce 模型来查询，等等。总而言之，虽然我们同意 CouchDB 背后的大部分理念，但我们认为在实践中，其实现并没有能够实现本地优先的愿景。&lt;/p&gt;&lt;h1 id=&quot;迈向更美好的未来&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E8%BF%88%E5%90%91%E6%9B%B4%E7%BE%8E%E5%A5%BD%E7%9A%84%E6%9C%AA%E6%9D%A5&quot; aria-label=&quot;迈向更美好的未来 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;迈向更美好的未来&lt;/h1&gt;&lt;p&gt;正如我们所表明的，现有的用于应用开发的数据层都不能完全满足本地优先的理想。因此，三年前，我们的实验室着手寻找一个能给出七个绿色复选标记的解决方案。&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;1. 快&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;2. 多设备&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;3. 离线&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;4. 协作&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;5. 长期可用性&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;6. 隐私&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;7. 控制权&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;???&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;我们已经发现了一些有前景的能满足本地优先理念的基础技术。最值得注意的是被称为&lt;a href=&quot;https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type&quot;&gt;无冲突复制数据类型（CRDTs）&lt;/a&gt;的分布式系统算法系列。&lt;/p&gt;&lt;h2 id=&quot;以-crdts-为基础性技术&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E4%BB%A5-crdts-%E4%B8%BA%E5%9F%BA%E7%A1%80%E6%80%A7%E6%8A%80%E6%9C%AF&quot; aria-label=&quot;以 crdts 为基础性技术 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;以 CRDTs 为基础性技术&lt;/h2&gt;&lt;p&gt;CRDTs 是&lt;a href=&quot;https://pages.lip6.fr/Marc.Shapiro/papers/RR-7687.pdf&quot;&gt;在 2011 年&lt;/a&gt;从学术计算机科学研究中出现的。它们是通用的数据结构，就像哈希图和列表一样，但它们的特殊之处在于，它们从一开始就支持多用户。&lt;/p&gt;&lt;p&gt;每个应用程序都需要一些数据结构来存储其文档状态。例如，如果你的应用程序是文本编辑器，其核心数据结构是构成文档的字符数组。如果你的应用程序是电子表格，数据结构是一个包含文本、数字或引用其他单元格的公式的单元格矩阵。如果它是矢量图形应用程序，数据结构是一个图形对象的树，如文本对象、矩形、线和其他形状。&lt;/p&gt;&lt;p&gt;如果你正在建立单用户应用程序，你将使用模型对象、哈希图、列表、记录/结构等在内存中维护这些数据结构。如果你正在建立协作的多用户应用程序，你可以把这些数据结构换成 CRDTs。&lt;/p&gt;&lt;p&gt;
  &lt;img src=&quot;https://www.inkandswitch.com/local-first/static/crdt-example.png&quot; style=&quot;filter:invert(1)&quot;/&gt;
  &lt;em&gt;两个设备最初拥有相同的待办事项列表。在设备1上，使用 .push() 方法将一个新的项目添加到列表中，该方法将新的项目附加到列表的末尾。同时，第一个项目在设备2上被标记为完成。在两个设备进行通信后，CRDT自动合并状态，使两个变化都生效。&lt;/em&gt;
&lt;/p&gt;&lt;p&gt;上图显示了一个由&lt;a href=&quot;http://arxiv.org/abs/1608.03960&quot;&gt;具有 JSON 数据模型的 CRDT &lt;/a&gt;支持的待办事项列表应用程序的例子。即使是在离线状态下用户也可以在他们的本地设备上查看和修改应用程序的状态。CRDT 会跟踪所做的任何修改，并在有网络连接时在后台与其他设备同步这些修改。&lt;/p&gt;&lt;p&gt;如果状态在不同的设备上被同时修改，CRDT 会合并这些变更。例如，如果用户在不同的设备上同时向待办事项列表添加新的项目，合并后的状态会以一致的顺序包含所有添加的项目。对不同对象的并发变化也很容易被合并。CRDT 不能自动解决的唯一变化类型是当多个用户同时更新同一对象的同一属性时；在这种情况下，CRDT 会跟踪冲突的值，并让它由应用程序或用户来解决。&lt;/p&gt;&lt;p&gt;因此，CRDTs 与 Git 这样的版本控制系统有一些相似之处，只是它们操作的数据类型比文本更丰富。CRDTs 可以通过任何通信渠道（例如，通过服务器，通过点对点连接，通过本地设备之间的蓝牙，甚至在 U 盘上）同步其状态。CRDT 可以追踪每一次按键，从而实现 Google Docs 式的实时协作。你也可以汇集更多改动，并把它们作为一个批次发送给合作者，这就像是 Git 中的 Pull Request。因为数据结构是通用的，所以我们可以开发通用的工具来存储、通信和管理 CRDTs，使我们不必在每个应用中重新实现这些东西。&lt;/p&gt;&lt;p&gt;对于 CRDTs 的更多技术介绍，我们建议以下内容：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Alexei Baboulevitch’s &lt;a href=&quot;http://archagon.net/blog/2018/03/24/data-laced-with-history/&quot;&gt;Data Laced with History&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Martin Kleppmann’s &lt;a href=&quot;https://www.inkandswitch.com/local-first/#user-control:~:text=Convergence%20vs%20Consensus&quot;&gt;Convergence vs Consensus&lt;/a&gt; (&lt;a href=&quot;https://speakerdeck.com/ept/convergence-versus-consensus-crdts-and-the-quest-for-distributed-consistency&quot;&gt;slides&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Shapiro et al.’s &lt;a href=&quot;https://www.inkandswitch.com/local-first/#user-control:~:text=comprehensive%20survey&quot;&gt;comprehensive survey&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Attiya et al.’s &lt;a href=&quot;http://software.imdea.org/~gotsman/papers/editing-podc16.pdf&quot;&gt;formal specification of collaborative text editing&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Gomes et al.’s &lt;a href=&quot;https://dl.acm.org/citation.cfm?doid=3152284.3133933&quot;&gt;formal verification of CRDTs&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Ink &amp;amp; Switch 已经开发了一个开源的 JavaScript CRDT 实现，名为 &lt;a href=&quot;https://github.com/automerge/automerge&quot;&gt;Automerge&lt;/a&gt;。它是基于我们早期对 JSON CRDTs 的研究。然后，我们将 Automerge 与 &lt;a href=&quot;https://datproject.org/&quot;&gt;Dat 网络栈&lt;/a&gt;相结合，形成了 &lt;a href=&quot;https://github.com/automerge/hypermerge&quot;&gt;Hypermerge&lt;/a&gt;。我们并不声称这些库完全实现了本地优先的理想—仍然需要更多的工作。&lt;/p&gt;&lt;p&gt;然而，基于我们对它们的经验，我们相信 CRDTs 有可能成为新一代软件的基础。就像分组交换是互联网和网络的促成技术，或者电容式触摸屏是智能手机的促成技术一样，我们认为 CRDTs 可能是本地优先软件的基础技术。&lt;/p&gt;&lt;h1 id=&quot;ink--switch-原型&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#ink--switch-%E5%8E%9F%E5%9E%8B&quot; aria-label=&quot;ink  switch 原型 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Ink &amp;amp; Switch 原型&lt;/h1&gt;&lt;p&gt;虽然学术研究在设计 CRDT 的算法和验证其理论正确性方面取得了很好的进展，但到目前为止，这些技术的工业应用相对较少。此外，大多数工业 CRDT 的使用都是在以服务器为中心的计算中，但我们相信这项技术在客户端的创造性工作的应用中有着巨大的潜力。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;使用 CRDTs 的以服务器为中心的系统包括 Azure Cosmos DB、Redis、Riak、Weave Mesh、SoundCloud 的 Roshi 和 Facebook 的 OpenR。然而，我们对 CRDTs 在终端用户设备上的使用最感兴趣。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;这也是&lt;a href=&quot;https://www.inkandswitch.com/&quot;&gt;我们的实验室&lt;/a&gt;开始进行一系列实验原型的动机，这些实验原型是建立在 CRDTs 上的具有协作性的本地优先应用。每个原型都以现有的创造性工作的应用程序（如 Trello、Figma 或 Milanote）为模型，提供终端用户体验。&lt;/p&gt;&lt;p&gt;这些实验探索了三个方面的问题。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;技术的可行性&lt;/strong&gt;。CRDTs 在多大程度上可以用于工作软件？我们需要什么来进行网络通信，或安装软件的开始？&lt;/li&gt;&lt;li&gt;&lt;strong&gt;用户体验&lt;/strong&gt;。本地优先的软件使用起来感觉如何？我们能否在没有权威的集中式服务器的情况下获得类似 Google Docs 的无缝实时协作体验？对于源代码以外的数据类型，类似 Git 的、离线的、异步的协作体验如何？一般来说，没有集中式服务器，用户界面会有什么不同？&lt;/li&gt;&lt;li&gt;&lt;strong&gt;开发者体验&lt;/strong&gt;。对于一个应用程序的开发者来说，使用基于 CRDT 的数据层与现有的存储层（如 SQL 数据库、文件系统或 Core Data）相比如何？分布式系统是否更难编写软件？我们是否需要模式和类型检查？开发人员将用什么来调试和反省他们应用程序的数据层？&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;我们使用 &lt;a href=&quot;https://electronjs.org/&quot;&gt;Electron&lt;/a&gt;、JavaScript 和 &lt;a href=&quot;https://reactjs.org/&quot;&gt;React&lt;/a&gt; 建立了三个原型。这给了我们快速开发能力，同时也给了我们的用户一个可以下载和安装的软件，我们发现这是本地优先的一个重要部分。&lt;/p&gt;&lt;h2 id=&quot;看板&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E7%9C%8B%E6%9D%BF&quot; aria-label=&quot;看板 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;看板&lt;/h2&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/automerge/trellis#readme&quot;&gt;Trellis&lt;/a&gt; 是一个以流行的 &lt;a href=&quot;https://trello.com/&quot;&gt;Trello&lt;/a&gt; 项目管理软件为模型的&lt;a href=&quot;https://en.wikipedia.org/wiki/Kanban_board&quot;&gt;看板&lt;/a&gt;。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/trellis.png&quot;/&gt;
&lt;em&gt;Trellis 提供了一个类似于 Trello 的本地优先软件的体验。右边的修改历史反映了所有在该文件中活跃的用户所做的修改&lt;/em&gt;&lt;/p&gt;&lt;p&gt;在这个项目中，我们尝试使用 &lt;a href=&quot;https://webrtc.org/&quot;&gt;WebRTC&lt;/a&gt; 作为网络通信层。&lt;/p&gt;&lt;p&gt;在用户体验方面，我们设计了一个初级的 “变更历史”，其灵感来自于 Git 和 Google Docs 的“&lt;a href=&quot;https://support.google.com/docs/answer/190843&quot;&gt;查看新的变更&lt;/a&gt;”，允许用户查看他们看板上的操作。这包括回溯时间，查看文件的早期状态。&lt;/p&gt;&lt;p&gt;通过&lt;a href=&quot;https://www.youtube.com/watch?v=L9fdyDlhByM&quot;&gt;演示视频&lt;/a&gt;观看 Trellis 的操作，或者&lt;a href=&quot;https://github.com/automerge/trellis/releases&quot;&gt;下载&lt;/a&gt;亲自尝试。&lt;/p&gt;&lt;h2 id=&quot;协作绘画&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%8D%8F%E4%BD%9C%E7%BB%98%E7%94%BB&quot; aria-label=&quot;协作绘画 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;协作绘画&lt;/h2&gt;&lt;p&gt;PixelPusher 是一个协作绘图程序，为 Javier Valencia 的 Pixel Art to CSS 带来类似 Figma 的实时体验。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/pixelpusher.png&quot;/&gt;
&lt;em&gt;多人实时同步地绘制。顶部的 URL 提供了与其他用户分享该文件的快速方法。右边的 “版本”面板显示了当前文档的所有分支。箭头按钮提供了分支之间的即时合并&lt;/em&gt;&lt;/p&gt;&lt;p&gt;在这个项目中，我们通过 &lt;a href=&quot;https://datproject.org/&quot;&gt;Dat 项目&lt;/a&gt;的点对点库进行了网络通信的实验。&lt;/p&gt;&lt;p&gt;用户体验实验包括用于文档共享的 URL，受 Git 启发的可视化分支/合并工具，用红色突出冲突像素的冲突解决机制，以及通过用户绘制的头像的基本用户身份。&lt;/p&gt;&lt;p&gt;阅读&lt;a href=&quot;https://www.inkandswitch.com/local-first/#user-control:~:text=Read%20the-,full%20project%20report,-or%20download%20a&quot;&gt;完整的项目报告&lt;/a&gt;或&lt;a href=&quot;https://github.com/automerge/pixelpusher/releases&quot;&gt;下载&lt;/a&gt;亲自尝试。&lt;/p&gt;&lt;h2 id=&quot;多媒体画布&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%A4%9A%E5%AA%92%E4%BD%93%E7%94%BB%E5%B8%83&quot; aria-label=&quot;多媒体画布 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;多媒体画布&lt;/h2&gt;&lt;p&gt;&lt;a href=&quot;https://inkandswitch.github.io/pushpin/&quot;&gt;PushPin&lt;/a&gt; 是一个类似于 &lt;a href=&quot;https://miro.com/&quot;&gt;Miro&lt;/a&gt; 或 &lt;a href=&quot;https://www.milanote.com/&quot;&gt;Milanote&lt;/a&gt; 的混合媒体画布工作空间。作为我们在 Automerge 上建立的第三个项目，它是这三个项目中实现得最充分的一个。我们的团队和外部测试用户的实际使用对底层数据层造成了更大的压力。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/pushpin.jpg&quot;/&gt;
&lt;em&gt;PushPin的画布混合了文本、图像、讨论线程和网络链接。用户通过工具栏上的头像看到对方，并使用URL栏在他们自己的文件之间导航&lt;/em&gt;&lt;/p&gt;&lt;p&gt;PushPin 探索了嵌套和连接的共享文档、CRDT 文档的各种渲染器、更先进的身份系统（包括用于共享的 “发件箱”模型），以及对共享短暂数据的支持，如选择高亮。&lt;/p&gt;&lt;p&gt;观看 &lt;a href=&quot;https://www.youtube.com/watch?v=Dox3XAoTCyg&quot;&gt;PushPin 的演示视频&lt;/a&gt;或&lt;a href=&quot;https://github.com/inkandswitch/pushpin/releases&quot;&gt;下载&lt;/a&gt;亲自尝试。&lt;/p&gt;&lt;h2 id=&quot;发现&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%8F%91%E7%8E%B0&quot; aria-label=&quot;发现 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;发现&lt;/h2&gt;&lt;p&gt;我们开发 Trellis、PixelPusher 和 PushPin 三个原型的目的是为了评估技术可行性、用户体验以及本地优先软件和 CRDTs 的开发者经验。我们通过在开发团队（由五名成员组成）中定期使用这些原型，对我们开发软件的经验进行批判性的反思，以及对大约十名外部用户进行单独的可用性测试来测试这些原型。外部用户包括专业设计师、产品经理和软件工程师。我们没有遵正式的评估方法，而是采取了一种探索性的方法来发现我们原型的优点和缺点。&lt;/p&gt;&lt;p&gt;在这一节中，我们概述了我们从建立和使用这些原型中得到的教训。虽然这些发现有些主观，但我们相信它们包含了有价值的见解，因为我们比其他项目在基于 CRDT 的本地优先的成熟产品道路上走得更远。&lt;/p&gt;&lt;h3 id=&quot;crdt-技术管用&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#crdt-%E6%8A%80%E6%9C%AF%E7%AE%A1%E7%94%A8&quot; aria-label=&quot;crdt 技术管用 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CRDT 技术管用&lt;/h3&gt;&lt;p&gt;从一开始，我们就对 Automerge 的可靠性感到惊喜。我们团队中的应用程序开发人员能够相对容易地整合该库，而且数据的自动合并几乎总是直接和无缝的。&lt;/p&gt;&lt;h3 id=&quot;离线工作的用户体验非常出色&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E7%A6%BB%E7%BA%BF%E5%B7%A5%E4%BD%9C%E7%9A%84%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C%E9%9D%9E%E5%B8%B8%E5%87%BA%E8%89%B2&quot; aria-label=&quot;离线工作的用户体验非常出色 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;离线工作的用户体验非常出色&lt;/h3&gt;&lt;p&gt;下线，继续工作，然后重新连接，与同事同步，这一切都运行良好。当系统上的其他应用程序抛出错误（“离线！警告！“）并阻止用户工作时，本地优先的原型仍正常工作，不受网络状态的影响。与基于浏览器的系统不同，它永远不需要对应用程序是否能工作或数据是否在那里感到焦虑。这让用户对他们的工具和他们的工作有一种「拥有」的感受，正如我们所希望的那样。&lt;/p&gt;&lt;h3 id=&quot;结合-crdt-与-functional-reactive-programming-frp-后的开发体验不错&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E7%BB%93%E5%90%88-crdt-%E4%B8%8E-functional-reactive-programming-frp-%E5%90%8E%E7%9A%84%E5%BC%80%E5%8F%91%E4%BD%93%E9%AA%8C%E4%B8%8D%E9%94%99&quot; aria-label=&quot;结合 crdt 与 functional reactive programming frp 后的开发体验不错 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;结合 CRDT 与 &lt;a href=&quot;http://people.seas.harvard.edu/~chong/pubs/pldi13-elm.pdf&quot;&gt;Functional Reactive Programming&lt;/a&gt; (FRP) 后的开发体验不错&lt;/h3&gt;&lt;p&gt;React 的 FRP 模型与 CRDTs 很适合。基于 CRDTs 的数据层意味着用户的文档同时获得来自本地用户的更新（例如，当他们在文本文档中输入时），但也来自网络（当其他用户和其他设备对文档进行更改时）。&lt;/p&gt;&lt;p&gt;因为 FRP 模型能可靠地同步「应用程序的可见状态」与「共享文档的底层状态」，开发者可以从同步视图状态和文档状态的繁琐工作中解脱出来。另外，通过确保对底层状态的所有改变都是通过一个单一的函数（一个 “reducer”）进行的，很容易确保所有相关的本地改变都被发送给其他用户。&lt;/p&gt;&lt;p&gt;这使应用开发者毫不费力地让所有原型都实现了实时协作和离线使用能力。这是显著的优势，因为它使应用程序开发人员能够专注于他们的应用程序，而不是解决数据分发的挑战。&lt;/p&gt;&lt;h3 id=&quot;冲突并不像我们担心的那样是一个重大问题&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%86%B2%E7%AA%81%E5%B9%B6%E4%B8%8D%E5%83%8F%E6%88%91%E4%BB%AC%E6%8B%85%E5%BF%83%E7%9A%84%E9%82%A3%E6%A0%B7%E6%98%AF%E4%B8%80%E4%B8%AA%E9%87%8D%E5%A4%A7%E9%97%AE%E9%A2%98&quot; aria-label=&quot;冲突并不像我们担心的那样是一个重大问题 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;冲突并不像我们担心的那样是一个重大问题&lt;/h3&gt;&lt;p&gt;我们经常被问及自动合并的有效性，许多人认为需要特定的应用冲突解决机制。然而我们出乎意料地发现用户在与他人协作时，很少遇到编辑冲突，而通用的冲突解决机制则运作良好。这方面的原因是：&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Automerge 在一个细粒度的层面上跟踪变化，并考虑到数据类型语义。例如，如果两个用户同时在一个数组的相同位置插入项目，Automerge 将这些变化结合起来，以确定的顺序定位这两个新项目。相比之下，像 Git 这样的文本版本控制系统会将这种情况视为需要手动解决的冲突。&lt;/li&gt;&lt;li&gt;用户对人类的协作有一种直观的感觉，并避免与他们的合作者产生冲突。例如，当用户合作编辑一篇文章时，他们可能会事先约定在一段时间内由谁来处理哪个部分，并避免同时修改同一个部分。&lt;/li&gt;&lt;li&gt;当不同的用户同时修改文档状态的不同部分时，Automerge 会干净利落地合并这些修改，没有任何困难。以看板应用为例，一个用户可以在一张卡片上发表评论，另一个用户可以将其移动到另一列，合并后的结果将反映这两个变化。只有当用户同时修改同一对象的相同属性时才会产生冲突：例如，如果两个用户同时改变画布上同一图像对象的位置。在这种情况下，如何解决它们往往是任意的，无论哪种方式都令人满意。&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Automerge 的数据结构带有针对并发变化的默认解决策略。原则上，人们可能期望不同的应用需要不同的合并语义。然而，在我们开发的所有原型中，我们发现默认的合并语义已经足够了，而且到目前为止我们还没有发现任何需要定制语义的情况。我们假设情况普遍如此，我们希望未来的研究能够进一步检验这一假设。 &lt;/p&gt;&lt;h3 id=&quot;将文件历史可视化是很重要的&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%B0%86%E6%96%87%E4%BB%B6%E5%8E%86%E5%8F%B2%E5%8F%AF%E8%A7%86%E5%8C%96%E6%98%AF%E5%BE%88%E9%87%8D%E8%A6%81%E7%9A%84&quot; aria-label=&quot;将文件历史可视化是很重要的 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;将文件历史可视化是很重要的&lt;/h3&gt;&lt;p&gt;在分布式协作系统中，另一个用户可以在任何时候向你传递任何数量的变化。与集中式系统不同的是，服务器对变化进行调解，本地优先的应用程序需要为这些问题找到自己的解决方案。如果没有合适的工具，就很难了解一个文件是如何形成的，文件有哪些版本，或者各部分都是由谁编写的。&lt;/p&gt;&lt;p&gt;在 Trellis 项目中，我们尝试使用了一个 “时间旅行”的界面，允许用户回到过去，查看合并后文件的早期状态，并在收到其他用户的修改时自动突出最近的修改内容。以线性方式穿越潜在的复杂的合并文件历史的能力有助于提供上下文，并可能成为理解协作的通用工具。&lt;/p&gt;&lt;h3 id=&quot;url-是很好的分享机制&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#url-%E6%98%AF%E5%BE%88%E5%A5%BD%E7%9A%84%E5%88%86%E4%BA%AB%E6%9C%BA%E5%88%B6&quot; aria-label=&quot;url 是很好的分享机制 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;URL 是很好的分享机制&lt;/h3&gt;&lt;p&gt;我们试验了许多与其他用户共享文件的机制，发现受网络启发的 URL 模型对用户和开发者来说最有意义。URL 可以被复制和粘贴，并通过电子邮件或聊天等交流渠道进行分享。秘密 URL 之外的文件的访问权限仍然是开放的研究问题。&lt;/p&gt;&lt;h3 id=&quot;peer-to-peer-系统从未完全-在线-或-离线并且很难推测数据如何流动&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#peer-to-peer-%E7%B3%BB%E7%BB%9F%E4%BB%8E%E6%9C%AA%E5%AE%8C%E5%85%A8-%E5%9C%A8%E7%BA%BF-%E6%88%96-%E7%A6%BB%E7%BA%BF%E5%B9%B6%E4%B8%94%E5%BE%88%E9%9A%BE%E6%8E%A8%E6%B5%8B%E6%95%B0%E6%8D%AE%E5%A6%82%E4%BD%95%E6%B5%81%E5%8A%A8&quot; aria-label=&quot;peer to peer 系统从未完全 在线 或 离线并且很难推测数据如何流动 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Peer-to-peer 系统从未完全 “在线” 或 “离线”，并且很难推测数据如何流动&lt;/h3&gt;&lt;p&gt;传统的集中式系统中通常有「在线」或「离线」的状态，这些状态由每个客户端根据与服务器保持稳定网络连接的能力来定义。服务器决定了真实数据是什么。&lt;/p&gt;&lt;p&gt;在去中心化的系统中，我们的数据可以有万花筒般的复杂性。任何用户都可能对他们所拥有、选择分享或接受的数据有不同的看法。例如，用户对一份文件的编辑可能在飞机上的笔记本电脑上；当飞机降落，电脑重新连接时，这些修改就会分发给其他用户。其他用户可能会选择接受所有、部分或不接受这些对他们版本的文件的修改。&lt;/p&gt;&lt;p&gt;文件的不同版本会导致混乱。就像 Git 仓库一样，特定的用户在 “主”分支中看到的东西是他们最后一次与其他用户交流时的功能。新来的修改可能会意外地修改你正在工作的文档的一部分，但手动合并每个用户的每一个修改是很乏味的。去中心化的文档使用户能够控制他们自己的数据，但需要进一步研究以了解它对实际应用中 UI 的影响。&lt;/p&gt;&lt;h3 id=&quot;crdts-积累了大量的变化历史产生了性能问题&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#crdts-%E7%A7%AF%E7%B4%AF%E4%BA%86%E5%A4%A7%E9%87%8F%E7%9A%84%E5%8F%98%E5%8C%96%E5%8E%86%E5%8F%B2%E4%BA%A7%E7%94%9F%E4%BA%86%E6%80%A7%E8%83%BD%E9%97%AE%E9%A2%98&quot; aria-label=&quot;crdts 积累了大量的变化历史产生了性能问题 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CRDTs 积累了大量的变化历史，产生了性能问题&lt;/h3&gt;&lt;p&gt;我们的团队将 PushPin 用于类似 Sprint 计划的真实文档中。性能和内存/磁盘的使用很快就成了问题，因为 CRDTs 存储了所有的历史，包括逐个字符的文本编辑。这些数据会堆积起来，不能轻易地被截断，因为不可能知道什么时候有人会在离开 6 个月后重新连接到你的共享文档，并需要从那一刻开始合并修改。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;注：与文章发表时相比 CRDT 的性能和内存/磁盘占用已经有了明显改善，详情可参考 &lt;a href=&quot;https://github.com/dmonad/crdt-benchmarks&quot;&gt;CRDT benchmarks&lt;/a&gt; &lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&quot;网络通信仍然是未解决的问题&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E4%BB%8D%E7%84%B6%E6%98%AF%E6%9C%AA%E8%A7%A3%E5%86%B3%E7%9A%84%E9%97%AE%E9%A2%98&quot; aria-label=&quot;网络通信仍然是未解决的问题 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;网络通信仍然是未解决的问题&lt;/h3&gt;&lt;p&gt;CRDT 算法只规定了数据的合并，但没有说不同用户的编辑如何到达同一台物理计算机上。&lt;/p&gt;&lt;p&gt;在这些实验中，我们尝试了通过 WebRTC 进行网络通信；用 Dropbox 和 USB 钥匙复制文件的 &lt;a href=&quot;https://en.wikipedia.org/wiki/Sneakernet&quot;&gt;“sneakernet”&lt;/a&gt;实现；可能使用 &lt;a href=&quot;https://ipfs.io/&quot;&gt;IPFS&lt;/a&gt; 协议；并最终确定使用 &lt;a href=&quot;https://datproject.org/&quot;&gt;Dat&lt;/a&gt; 的 &lt;a href=&quot;https://github.com/mafintosh/hypercore&quot;&gt;Hypercore&lt;/a&gt; 点对点库。&lt;/p&gt;&lt;p&gt;CRDTs 不需要点对点网络层；使用服务器进行通信对 CRDTs 来说是很好的。然而，为了充分实现本地优先软件的长期有效的目标，我们希望应用程序的寿命超过其供应商管理的任何后端服务，所以去中心化的解决方案是合理的最终目标。&lt;/p&gt;&lt;p&gt;对于原型中使用的 P2P 技术我们发现：一方面，这些技术还远未达到可以投入生产的程度。特别是 &lt;a href=&quot;https://tools.ietf.org/html/rfc5389&quot;&gt;NAT 穿透&lt;/a&gt;，是不可靠的，这取决于用户当前连接的特定路由器或网络拓扑结构。但在另一方面，P2P 协议和&lt;a href=&quot;https://decentralizedweb.net/&quot;&gt;去中心化网络&lt;/a&gt;社区所带来的可能性是巨大的。在这个已经开始依赖中心化 API 的世界里，没有互联网接入的计算机之间的实时协作感觉就像魔术一样。&lt;/p&gt;&lt;h3 id=&quot;云服务器在发现备份和突发计算方面仍有其地位&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E4%BA%91%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%9C%A8%E5%8F%91%E7%8E%B0%E5%A4%87%E4%BB%BD%E5%92%8C%E7%AA%81%E5%8F%91%E8%AE%A1%E7%AE%97%E6%96%B9%E9%9D%A2%E4%BB%8D%E6%9C%89%E5%85%B6%E5%9C%B0%E4%BD%8D&quot; aria-label=&quot;云服务器在发现备份和突发计算方面仍有其地位 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;云服务器在发现、备份和突发计算方面仍有其地位&lt;/h3&gt;&lt;p&gt;像 PushPin 这样的实时协作原型可以让用户在没有中间服务器的情况下与其他用户分享他们的文件。这对于隐私和所有权来说是非常好的，但是可能会出现这样的情况：一个用户分享了一个文件，然后在其他用户连接之前关闭了他们的笔记本盖子。如果用户没有同时在线，他们就不能互相连接。&lt;/p&gt;&lt;p&gt;因此，服务器在本地优先的世界中可以发挥作用—不是作为中央机构，而是作为 “cloud peer” 来支持客户端应用程序（它将不在关键路径上）。例如，一个存储文件副本的 cloud peer，在其他 peer 上线时将其转发给他们，可以解决上述封闭的笔记本电脑问题。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://hashbase.io/&quot;&gt;Hashbase&lt;/a&gt; 是 Cloud Peer 的例子，也是 &lt;a href=&quot;https://datproject.org/&quot;&gt;Dat&lt;/a&gt; 和 &lt;a href=&quot;https://beakerbrowser.com/&quot;&gt;Beaker 浏览器&lt;/a&gt;的桥梁。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;同样地，Cloud Peer 可以是&lt;/p&gt;&lt;ul&gt;&lt;li&gt;存档/备份位置（特别是对于手机或其他存储空间有限的设备）&lt;/li&gt;&lt;li&gt;通往传统服务器 API 的桥梁（如天气预报或股票行情）&lt;/li&gt;&lt;li&gt;突发计算资源的提供者（如使用强大的 GPU 渲染视频）&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;传统系统和本地优先系统的关键区别不是没有服务器，而是服务器的职责发生了变化：它们属于支持性角色，而不是真理的来源。&lt;/p&gt;&lt;h1 id=&quot;你可以如何提供帮助&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E4%BD%A0%E5%8F%AF%E4%BB%A5%E5%A6%82%E4%BD%95%E6%8F%90%E4%BE%9B%E5%B8%AE%E5%8A%A9&quot; aria-label=&quot;你可以如何提供帮助 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;你可以如何提供帮助&lt;/h1&gt;&lt;p&gt;这些实验表明，本地优先的软件是可能的。协作和所有权并不相悖—我们可以得到两个世界的最好结果，用户也可以从中受益。&lt;/p&gt;&lt;p&gt;然而，基础技术仍在进行。它们对开发原型很有好处，我们希望它们在未来几年内能够发展和稳定下来，但从现实的角度来看，今天在生产环境中用 Automerge 这样的实验项目来取代 Firebase 这样的成熟产品还不可取。&lt;/p&gt;&lt;p&gt;如果你和我们一样相信本地优先的未来，你（和我们所有技术领域的人）可以做什么来推动我们走向这个未来？这里有一些建议。&lt;/p&gt;&lt;h2 id=&quot;对于分布式系统和编程语言研究者来说&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%AF%B9%E4%BA%8E%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E5%92%8C%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E7%A0%94%E7%A9%B6%E8%80%85%E6%9D%A5%E8%AF%B4&quot; aria-label=&quot;对于分布式系统和编程语言研究者来说 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;对于分布式系统和编程语言研究者来说&lt;/h2&gt;&lt;p&gt;本地优先的软件从最近对分布式系统的研究中受益匪浅，包括 CRDTs 和 P2P 技术。目前的研究界在提高 CRDTs 的性能和功率方面取得了卓越的进展，我们热切地等待着这项工作的进一步成果。尽管如此，仍有一些有趣的机会可以进一步开展工作。&lt;/p&gt;&lt;p&gt;大多数 CRDT 研究的运作模式是，所有合作者立即将他们的编辑应用于某个文件的单一版本。然而，实际的本地优先应用需要更多的灵活性：用户必须有拒绝其他合作者编辑的自由，或者对不与他人共享的文件版本进行私人修改。用户可能想决定应用哪些修改，或调整修改历史。这些概念在分布式源码控制领域被很好地理解为 “分支”、“分叉”、“重定位”等等。迄今为止，只有很少的算法和编程模型的工作来解决在多个文档版本和分支并存的情况下的问题。&lt;/p&gt;&lt;p&gt;还有围绕类型、模式迁移和兼容性的更深入的有意思的问题。不同的合作者可能使用不同版本的应用程序，可能具有不同的功能。由于没有中央数据库服务器，所以没有权威的“当前”数据 Schema。我们如何编写软件，使不同的应用程序版本能够安全地交互，即使是在数据格式变化的时候？这个问题在基于云的 API 设计中也有类似之处，但在本地优先中有额外的挑战。&lt;/p&gt;&lt;h2 id=&quot;对于人机交互hci研究者来说&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%AF%B9%E4%BA%8E%E4%BA%BA%E6%9C%BA%E4%BA%A4%E4%BA%92hci%E7%A0%94%E7%A9%B6%E8%80%85%E6%9D%A5%E8%AF%B4&quot; aria-label=&quot;对于人机交互hci研究者来说 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;对于人机交互（HCI）研究者来说&lt;/h2&gt;&lt;p&gt;对于集中式系统，应用程序与服务器的“同步”状态很容易提供，也有大量案例可以参考。而在去中心化系统有很多新机会来探索 UI 上的挑战。&lt;/p&gt;&lt;p&gt;我们希望研究人员能够考虑如何提供在线和离线状态，或者在任何其他用户可能持有不同数据副本的系统中的可用和不可用状态。当每个人都是 peer 的时候，我们应该如何考虑网络连接的问题？当我们可以直接与其他节点同步而不需要访问互联网时，“在线”意味着什么？&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.inkandswitch.com/local-first/static/git-history.png&quot;/&gt;
&lt;em&gt;“铁轨”模型。它在 GitX 中被用于可视化 Git 仓库中的源代码历史结构&lt;/em&gt;&lt;/p&gt;&lt;p&gt;当每个文件都能形成复杂的版本历史时，仅仅通过日常操作，就会出现一个尖锐的问题：我们如何将这个版本历史传达给用户？在没有中心化的真理源的情况下，用户应该如何思考版本问题，分享和接受修改，并理解他们的文档是如何变成某种样子的？今天，有两种主流的变更管理模式：一种是差异和补丁的源代码模式，另一种是建议和评论的 Google Docs 模式。这些就是我们能做的最好的了吗？我们如何将这些想法推广到非文本的数据格式？我们急切地想知道能发现什么。&lt;/p&gt;&lt;p&gt;虽然集中式系统在很大程度上依赖于访问控制和权限，但同样的概念并不直接适用于本地优先的环境。例如，不能阻止任何拥有某些数据副本的用户在本地修改这些数据；但是，其他用户可以选择是否订阅这些修改。用户应该如何看待共享、权限和反馈？如果我们不能从别人的电脑上删除文件，那么与人 “停止共享”是什么意思？&lt;/p&gt;&lt;p&gt;我们相信，集中化的假设在我们今天的用户体验中根深蒂固，而我们才开始发现改变这种假设的后果。我们希望这些开放性的问题能够激发研究人员去探索我们认为是一个未开发的领域。 &lt;/p&gt;&lt;h2 id=&quot;对于从业者&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%AF%B9%E4%BA%8E%E4%BB%8E%E4%B8%9A%E8%80%85&quot; aria-label=&quot;对于从业者 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;对于从业者&lt;/h2&gt;&lt;p&gt;如果你是一名软件工程师、设计师、产品经理或独立的应用程序开发人员，你能提供什么帮助？我们建议采取循序渐进的步骤来实现本地优先的未来。从给你的应用程序打分开始：&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;1. 快&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;2. 多设备&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;3. 离线&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;4. 协作&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;5. 长期可用性&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;6. 隐私&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;7. 控制权&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;你的应用&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;?&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;?&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;?&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;?&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;?&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;?&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;?&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;这里是改善以上特性的一些策略：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;快&lt;/strong&gt;：激进的缓存和提前下载资源，可以防止用户在打开你的应用或之前打开的文档时看到”加载中”的状态。默认情况下信任本地缓存，而不是让用户等待网络获取。&lt;/li&gt;&lt;li&gt;&lt;strong&gt;多设备&lt;/strong&gt;：像 Firebase 和 iCloud 这样的同步基础设施使多设备支持变得相对简单，尽管它们确实引入了长期可用性和隐私问题。像 &lt;a href=&quot;https://docs.realm.io/sync/getting-started-1/getting-a-realm-object-server-instance&quot;&gt;Realm Object Server&lt;/a&gt; 这样的自我托管的基础设施提供了替代品。&lt;/li&gt;&lt;li&gt;&lt;strong&gt;离线&lt;/strong&gt;：在网络世界中，&lt;a href=&quot;https://developers.google.com/web/progressive-web-apps/&quot;&gt;Progressive Web Apps&lt;/a&gt; 提供的功能，如 Service Worker 和 App Manifests，可以提供帮助。在移动世界中，要注意 &lt;a href=&quot;https://developer.apple.com/documentation/webkit&quot;&gt;WebKit&lt;/a&gt; 框架和其他依赖网络的组件。通过关闭 WiFi 来测试你的应用程序，或使用 traffic shapers，如 &lt;a href=&quot;https://developers.google.com/web/tools/chrome-devtools/network/network-conditions&quot;&gt;Chrome Dev Tools 网络状况模拟器&lt;/a&gt;或 &lt;a href=&quot;https://nshipster.com/network-link-conditioner/&quot;&gt;iOS 网络链接调节器&lt;/a&gt;。&lt;/li&gt;&lt;li&gt;&lt;strong&gt;协作&lt;/strong&gt;：除了 CRDTs，更成熟的实时协作技术是 &lt;a href=&quot;http://www.codecommit.com/blog/java/understanding-and-applying-operational-transformation&quot;&gt;Operational Transformation（OT）&lt;/a&gt;，例如在 &lt;a href=&quot;https://github.com/share/sharedb&quot;&gt;ShareDB&lt;/a&gt; 中实现。&lt;/li&gt;&lt;li&gt;&lt;strong&gt;长期可用性&lt;/strong&gt;：确保你的软件可以很容易地导出到扁平化的标准格式，如 JSON 或 PDF。例如：大量导出，如 &lt;a href=&quot;https://takeout.google.com/settings/takeout&quot;&gt;Google Takeout&lt;/a&gt;；持续备份成稳定的文件格式，&lt;a href=&quot;https://support.goodnotes.com/hc/en-us/articles/202168425-How-should-I-backup-my-documents-&quot;&gt;如 GoodNotes&lt;/a&gt;；以及 JSON 下载文件，&lt;a href=&quot;https://help.trello.com/article/747-exporting-data-from-trello-1&quot;&gt;如 Trello&lt;/a&gt;。&lt;/li&gt;&lt;li&gt;&lt;strong&gt;隐私&lt;/strong&gt;：云计算应用程序从根本上说是非私有的，公司的员工和政府可以随时窥视用户数据。但对于移动或桌面应用，要尽量向用户说明数据何时只存储在他们的设备上，而不是传输到后端。&lt;/li&gt;&lt;li&gt;&lt;strong&gt;控制权&lt;/strong&gt;：用户能否在你的应用程序中轻松地备份、复制或删除他们的部分或全部文件？这通常需要重新实现所有基本的文件系统操作，就像 Google Docs 对 Google Drive 所做的那样。&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;对初创企业的呼吁&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%AF%B9%E5%88%9D%E5%88%9B%E4%BC%81%E4%B8%9A%E7%9A%84%E5%91%BC%E5%90%81&quot; aria-label=&quot;对初创企业的呼吁 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;对初创企业的呼吁&lt;/h2&gt;&lt;p&gt;如果你是一个对建立开发者基础设施感兴趣的企业家，上述所有情况表明了一个有趣的市场机会: “Firebase for CRDTs”。&lt;/p&gt;&lt;p&gt;这样的创业公司需要提供优异的开发者体验和在本地持久化的库（类似于 SQLite 或 Realm）。它需要适用于移动平台（iOS、Android）、本地桌面（Windows、Mac、Linux）和 Web（Electron、Progressive Web Apps）。&lt;/p&gt;&lt;p&gt;用户控制、隐私、多设备支持和协作都将被纳入其中。应用程序开发人员可以专注于构建他们的应用程序，因为他们知道最简单的实施路径也会给他们在本地优先的记分卡上打上最高分。作为检验你是否成功的试金石，我们建议：即使所有的服务器都关闭了，你的所有客户的应用程序是否能继续永久地工作？&lt;/p&gt;&lt;p&gt;我们相信，随着 CRDTs 时代的到来，“Firebase for CRDTs” 的机会将是巨大的。如果你正在做这方面的工作，我们想听听你的意见。&lt;/p&gt;&lt;h1 id=&quot;总结&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E6%80%BB%E7%BB%93&quot; aria-label=&quot;总结 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;总结&lt;/h1&gt;&lt;p&gt;计算机是人类有史以来最重要的创造性工具之一。软件已经成为我们完成工作的方式，也是工作的储存库。&lt;/p&gt;&lt;p&gt;为了追求更好的工具，我们将许多应用程序转移到云端。云计算软件在许多方面都优于“老式”软件：它提供了可协作的、永远最新的应用程序，可以从世界任何地方访问。我们不再担心我们正在运行的软件是什么版本，或某个文件在哪台机器上。&lt;/p&gt;&lt;p&gt;然而，在云中，数据的所有权属于服务器，而不是用户，因此我们成了自己数据的借用者。在云应用中创建的文件注定会在这些服务的创建者不再维护它们时消失。云服务是无法长期保存的。没有网站时光机可以恢复一个没落的网络应用。互联网档案馆不能保存你的谷歌文档。&lt;/p&gt;&lt;p&gt;在这篇文章中，我们为未来的软件探索了一条新的发展道路。我们展示了用户有可能保留对其数据的所有权和控制权，同时也受益于云相关的功能：无缝协作和从任何地方访问。鱼和熊掌可以兼得。&lt;/p&gt;&lt;p&gt;但还需要更多的工作才能将本地优先应用到实践中。应用程序开发人员可以采取渐进的步骤，例如改进离线支持和更好地利用设备上的存储。研究人员可以继续改进本地优先软件的算法、编程模型和用户界面。企业家们可以开发基础技术，如 CRDTs 和点对点网络，以驱动下一代应用。&lt;/p&gt;&lt;p&gt;今天，创建一个服务器掌控所有所有数据的网络应用是很容易的。但要建立尊重用户所有权和代理权的协作软件却太难了。为了改变这种平衡，我们需要改进开发本地优先软件的工具。我们希望你能加入我们。&lt;/p&gt;&lt;p&gt;我们欢迎你的想法，问题，或批评：&lt;a href=&quot;https://twitter.com/inkandswitch&quot;&gt;@inkandswitch&lt;/a&gt; 或 &lt;a href=&quot;mailto:hello@inkandswitch.com&quot;&gt;hello@inkandswitch.com&lt;/a&gt;。&lt;/p&gt;&lt;h1 id=&quot;致谢&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E8%87%B4%E8%B0%A2&quot; aria-label=&quot;致谢 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;致谢&lt;/h1&gt;&lt;p&gt;Martin Kleppmann 得到了波音公司的资助。感谢我们在 Ink &amp;amp; Switch 的合作者，他们参与了上面讨论的原型。Julia Roggatz, Orion Henry, Roshan Choxi, Jeff Peterson, Jim Pick, 和 Ignatius Gilfedder。也感谢 Heidi Howard、Roly Perera，以及 &lt;a href=&quot;https://2019.splashcon.org/track/splash-2019-Onward-Essays&quot;&gt;Onward！&lt;/a&gt; 的匿名审稿人对本文草稿的反馈。&lt;/p&gt;&lt;p&gt;本翻译也被发表至知乎&lt;a href=&quot;https://zhuanlan.zhihu.com/p/557353347&quot;&gt;见相关讨论&lt;/a&gt;。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[High-performance tidy trees visualization]]></title><description><![CDATA[This article introduces the algorithm to draw non-layered trees in linear time and re-layout partially when some nodes change in O(d) time…]]></description><link>https://www.zxch3n.com/tidy/tidy/</link><guid isPermaLink="false">https://www.zxch3n.com/tidy/tidy/</guid><pubDate>Tue, 14 Jun 2022 23:00:32 GMT</pubDate><content:encoded>&lt;p id=&quot;interactive_example&quot;&gt;&lt;/p&gt;&lt;div class=&quot;zxch3n-blog-container&quot;&gt;&lt;div style=&quot;display:flex;align-items:center;flex-direction:column;border:1px solid rgba(128,128,128,0.3);padding:16px;border-radius:8px;max-width:600px;box-shadow:0px 0px 8px rgba(128,128,128,0.3);position:relative;width:100%&quot;&gt;&lt;div style=&quot;width:100%;height:400px&quot;&gt;&lt;/div&gt;&lt;button style=&quot;position:absolute;top:12px;right:12px&quot; type=&quot;button&quot; class=&quot;ant-btn ant-btn-text ant-btn-sm ant-btn-icon-only&quot;&gt;&lt;svg width=&quot;1em&quot; height=&quot;1em&quot; viewBox=&quot;0 0 24 24&quot;&gt;&lt;path fill=&quot;#575757&quot; d=&quot;M12 20q-3.35 0-5.675-2.325Q4 15.35 4 12q0-3.35 2.325-5.675Q8.65 4 12 4q1.725 0 3.3.713q1.575.712 2.7 2.037V5q0-.425.288-.713Q18.575 4 19 4t.712.287Q20 4.575 20 5v5q0 .425-.288.712Q19.425 11 19 11h-5q-.425 0-.712-.288Q13 10.425 13 10t.288-.713Q13.575 9 14 9h3.2q-.8-1.4-2.187-2.2Q13.625 6 12 6Q9.5 6 7.75 7.75T6 12q0 2.5 1.75 4.25T12 18q1.725 0 3.188-.913q1.462-.912 2.187-2.437q.125-.275.413-.462q.287-.188.587-.188q.575 0 .863.4q.287.4.062.9q-.95 2.125-2.925 3.412Q14.4 20 12 20Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/button&gt;&lt;div style=&quot;color:grey;display:flex;flex-direction:row;margin-top:16px&quot;&gt;&lt;span style=&quot;margin-right:8px&quot;&gt;Naive&lt;/span&gt;&lt;button type=&quot;button&quot; role=&quot;switch&quot; aria-checked=&quot;true&quot; class=&quot;ant-switch ant-switch-checked&quot;&gt;&lt;div class=&quot;ant-switch-handle&quot;&gt;&lt;/div&gt;&lt;span class=&quot;ant-switch-inner&quot;&gt;&lt;/span&gt;&lt;/button&gt;&lt;span style=&quot;margin-left:8px&quot;&gt;Tidy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This article introduces the algorithm to draw non-layered trees in linear time and re-layout partially when some nodes change in O(d) time, where d is the maximum depth of the changed node.&lt;/p&gt;&lt;p&gt;The source code is available at &lt;a href=&quot;https://github.com/zxch3n/tidy&quot;&gt;zxch3n/tidy&lt;/a&gt;. It only takes a few milliseconds to finish the layout of a tree with tens of thousands of nodes within the web browser.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#introduction&quot;&gt;Introduction&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#layered-and-non-layered&quot;&gt;Layered and non-layered&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#aesthetic-rules&quot;&gt;Aesthetic rules&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#a-naive-non-layered-tree-visualization&quot;&gt;A naive non-layered tree visualization&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#tidy-tree-visualization-algorithm&quot;&gt;Tidy tree visualization algorithm&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#how-to-achieve-compactness&quot;&gt;How to achieve compactness&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#fix-aesthetic-rule-7&quot;&gt;Fix aesthetic rule 7&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#the-on-layout-algorithm-5&quot;&gt;The O(n) layout algorithm [5]&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#determine-move-distance&quot;&gt;Determine move distance&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#get-collided-subtree&quot;&gt;Get collided subtree&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#distribute-spacing&quot;&gt;Distribute spacing&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#final-code&quot;&gt;Final Code&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#interactive-example&quot;&gt;Interactive example&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#relayout-partially&quot;&gt;Relayout partially&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#benchmark&quot;&gt;Benchmark&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#future-work&quot;&gt;Future work&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#remaining-aesthetic-issue&quot;&gt;Remaining aesthetic issue&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#the-tidy-library&quot;&gt;The Tidy Library&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#references&quot;&gt;References&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;introduction&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#introduction&quot; aria-label=&quot;introduction permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Introduction&lt;/h1&gt;&lt;p&gt;Trees are ubiquitous data structures. Various visualizations of trees were proposed to help people understand different aspects of trees. This article focuses on the classical node-link diagrams.&lt;/p&gt;&lt;p&gt;A good tree drawing should be aesthetically pleasing while using as little space as possible. In 1979, Wetherell and Shannon [&lt;a href=&quot;#ref1&quot;&gt;1&lt;/a&gt;] formalized aesthetic rules for tidy trees and presented the first O(n) algorithm to solve it. In 1981, Reingold and Tilford [&lt;a href=&quot;#ref2&quot;&gt;2&lt;/a&gt;] extended the aesthetic rules to make the drawings more aesthetically pleasing and compact. However, both of them would &lt;a href=&quot;#fix-aesthetic-rule-7&quot;&gt;violate the aesthetic rules when extended to m-ary trees&lt;/a&gt;. In 1990, Walker [&lt;a href=&quot;#ref3&quot;&gt;3&lt;/a&gt;] solved this problem with O(n^2) time complexity. Buchheim [&lt;a href=&quot;#ref4&quot;&gt;4&lt;/a&gt;] improved Walker’s work to run in O(n) in 2002. In 2014, Ploeg [&lt;a href=&quot;#ref5&quot;&gt;5&lt;/a&gt;] presented the first O(n) algorithm to produce tidy non-layered drawings of trees.&lt;/p&gt;&lt;p&gt;This article introduces the layout algorithm of non-layered tidy trees [&lt;a href=&quot;#ref5&quot;&gt;5&lt;/a&gt;] and presents a fast relayout algorithm. Engineers can use it to build fast tree editor tools like mindmaps.&lt;/p&gt;&lt;h2 id=&quot;layered-and-non-layered&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#layered-and-non-layered&quot; aria-label=&quot;layered and non layered permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Layered and non-layered&lt;/h2&gt;&lt;p&gt;In tree visualization, each node may have variant width and height. For example, nodes may contain text content of different lengths. In this case, the tree can be drawn as layered, where nodes with the same depth are horizontally aligned, or non-layered, where there is a fixed vertical distance between parent and children.&lt;/p&gt;&lt;p&gt;Layered drawings make depth comparison easier, whereas non-layered ones generally use less space.&lt;/p&gt;&lt;div class=&quot;zxch3n-blog-container&quot;&gt;&lt;div class=&quot;ant-row&quot; style=&quot;max-width:1200px;width:100%&quot;&gt;&lt;div style=&quot;padding:4px&quot; class=&quot;ant-col ant-col-xs-24 ant-col-sm-24 ant-col-lg-12 ant-col-xl-12&quot;&gt;&lt;div style=&quot;height:400px;border:1px solid rgba(128,128,128,0.2);border-radius:8px&quot;&gt;&lt;div style=&quot;height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center&quot;&gt;&lt;div style=&quot;width:100%;height:100%;flex-grow:1&quot;&gt;&lt;/div&gt;&lt;p style=&quot;color:rgb(128, 128, 128);text-align:center;padding-bottom:8px&quot;&gt;Non-Layered&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;padding:4px&quot; class=&quot;ant-col ant-col-xs-24 ant-col-sm-24 ant-col-lg-12 ant-col-xl-12&quot;&gt;&lt;div style=&quot;height:400px;border:1px solid rgba(128,128,128,0.2);border-radius:8px&quot;&gt;&lt;div style=&quot;height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center&quot;&gt;&lt;div style=&quot;width:100%;height:100%&quot;&gt;&lt;/div&gt;&lt;p style=&quot;color:rgb(128, 128, 128);text-align:center;padding-bottom:8px&quot;&gt;Layered&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The algorithm of non-layered drawing is harder than the layered one since the former can be easily extended to the latter. Ploeg [&lt;a href=&quot;#ref5&quot;&gt;5&lt;/a&gt;] designed the first algorithm to finish the layout of non-layered tidy trees in linear time. The detail of the algorithm will be covered in the rest of this article. If you are interested in how layered drawing works, you can read the article by Bill Mill [&lt;a href=&quot;#ref6&quot;&gt;6&lt;/a&gt;].&lt;/p&gt;&lt;h2 id=&quot;aesthetic-rules&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#aesthetic-rules&quot; aria-label=&quot;aesthetic rules permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Aesthetic rules&lt;/h2&gt;&lt;p&gt;A tidy drawing of a tree should obey the aesthetic rules while using as little space as possible. The aesthetic rules include:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;No overlapped node&lt;/li&gt;&lt;li&gt;No crossed line&lt;/li&gt;&lt;li&gt;A node’s children should stay on the same line&lt;/li&gt;&lt;li&gt;Parents should be centered over their children&lt;/li&gt;&lt;li&gt;A subtree should be drawn the same way regardless of where it occurs in the tree&lt;/li&gt;&lt;li&gt;Nodes are ordered. Drawings of nodes should have the same order.&lt;/li&gt;&lt;li&gt;A tree and its mirror image should produce drawings that are reflections of one another, which implies&lt;ul&gt;&lt;li&gt;Small, interior subtrees should be spaced out evenly among larger subtrees, where the larger subtrees are adjacent at one or more levels.&lt;/li&gt;&lt;li&gt;Small subtrees at the far left or far right should be adjacent to larger subtrees.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ol&gt;&lt;h1 id=&quot;a-naive-non-layered-tree-visualization&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#a-naive-non-layered-tree-visualization&quot; aria-label=&quot;a naive non layered tree visualization permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;A naive non-layered tree visualization&lt;/h1&gt;&lt;p&gt;Most mind map applications use a naive version of the tidy layout, i.e., it obeys the aesthetic rules but does not care about compactness.&lt;/p&gt;&lt;p&gt;The naive tidy visualization algorithm treats each node and its offsprings as a bounding box, avoiding collision between the bounding boxes.&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;naiveTidyLayout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;root&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
  root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;preOrderTraverse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; margin
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postOrderTraverse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;bbox&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; childrenWidth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acc&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; acc &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; margin&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;margin
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;bbox&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; childrenWidth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; relativeX &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; childrenWidth &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; child &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      child&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;relativeX &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; relativeX &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; child&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;bbox&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; child&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
      relativeX &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; child&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;bbox&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; margin
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;preOrderTraverse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;x &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;x &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;relativeX
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It is easy to implement and has good performance. But its layout is not compact, as shown in &lt;a href=&quot;#interactive_example&quot;&gt;the interactive example&lt;/a&gt;.&lt;/p&gt;&lt;h1 id=&quot;tidy-tree-visualization-algorithm&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#tidy-tree-visualization-algorithm&quot; aria-label=&quot;tidy tree visualization algorithm permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Tidy tree visualization algorithm&lt;/h1&gt;&lt;h2 id=&quot;how-to-achieve-compactness&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#how-to-achieve-compactness&quot; aria-label=&quot;how to achieve compactness permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;How to achieve compactness&lt;/h2&gt;&lt;p&gt;Note that the aesthetic rules require that a subtree should be drawn the same way no matter where it appears in the tree. It means that if the layout of the subtree is done, inside it, all nodes’ relative positions to their parent are finalized. So we can use a post-order traversal to determine the relative positions. A high-level abstraction of the tree visualization algorithm is&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;layout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;root&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postOrderTraverse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// determine each child subtree&amp;#x27;s relative position to the parent&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;layoutSubtree&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;preOrderTraverse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;finalizeAbsolutePosition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To arrange children compactly, during &lt;code class=&quot;language-text&quot;&gt;layoutSubtree&lt;/code&gt;, we need child subtrees to be as close to their siblings as possible. Therefore, we can abstract a tidy layout algorithm as below.&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;layoutSubtree&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; children&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; prev &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; children&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cur &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; children&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    cur&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;relativeX &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getMoveDistance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    prev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;positionRoot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Suppose we are visualizing a complete k-ary tree, then we have &lt;span class=&quot;math math-inline&quot;&gt;T(n) = kT(n/k) + f(n)&lt;/span&gt;, where &lt;span class=&quot;math math-inline&quot;&gt;f(n)&lt;/span&gt; is the time complexity of the &lt;code class=&quot;language-text&quot;&gt;getMoveDistance&lt;/code&gt; function. Based on &lt;a href=&quot;https://en.wikipedia.org/wiki/Master_theorem_(analysis_of_algorithms)&quot;&gt;master theorem&lt;/a&gt;,&lt;/p&gt;&lt;ul&gt;&lt;li&gt;If &lt;span class=&quot;math math-inline&quot;&gt;f(n) = O(n^{1-ϵ})&lt;/span&gt; , then &lt;span class=&quot;math math-inline&quot;&gt;T(n) = Θ(n)&lt;/span&gt; .&lt;/li&gt;&lt;li&gt;If &lt;span class=&quot;math math-inline&quot;&gt;f(n) = Θ(n)&lt;/span&gt; , then &lt;span class=&quot;math math-inline&quot;&gt;T(n) = Θ(n * log n)&lt;/span&gt; .&lt;/li&gt;&lt;li&gt;If &lt;span class=&quot;math math-inline&quot;&gt;f(n) = Ω(n^{1+ϵ})&lt;/span&gt; , then &lt;span class=&quot;math math-inline&quot;&gt;T(n) = Θ(f(n))&lt;/span&gt; .&lt;/li&gt;&lt;li&gt;ϵ &amp;gt; 0 is a constant.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;We only need a distance function with complexity of &lt;span class=&quot;math math-inline&quot;&gt;O(n^{1-ϵ})&lt;/span&gt; to make the algorithm run in linear time. - Ploeg [&lt;a href=&quot;#ref5&quot;&gt;5&lt;/a&gt;] proposed a algorithm with &lt;span class=&quot;math math-inline&quot;&gt;f(n) = \log _kn =O(n^{1-ϵ})&lt;/span&gt; , so that &lt;span class=&quot;math math-inline&quot;&gt;T(n) = Θ(n)&lt;/span&gt;. Ploeg [&lt;a href=&quot;#ref5&quot;&gt;5&lt;/a&gt;] prooved its linear complexity for the cases other than complete k-ary tree.&lt;/p&gt;&lt;h2 id=&quot;fix-aesthetic-rule-7&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#fix-aesthetic-rule-7&quot; aria-label=&quot;fix aesthetic rule 7 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Fix aesthetic rule 7&lt;/h2&gt;&lt;p&gt;The above algorithm satisfies aesthetic rules 1-6, but not aesthetic rule 7: a tree and its mirror image should produce drawings that are reflections of one another, as shown in the image below.&lt;/p&gt;&lt;div class=&quot;zxch3n-blog-container&quot;&gt;&lt;div class=&quot;ant-row&quot; style=&quot;width:100%;max-width:800px&quot;&gt;&lt;div style=&quot;padding:4px&quot; class=&quot;ant-col ant-col-xs-24 ant-col-sm-24 ant-col-lg-12 ant-col-xl-12&quot;&gt;&lt;div style=&quot;height:200px;border:1px solid rgba(128,128,128,0.2);border-radius:8px&quot;&gt;&lt;div style=&quot;height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center&quot;&gt;&lt;div style=&quot;width:100%;flex-grow:1;padding:16px&quot;&gt;&lt;/div&gt;&lt;p style=&quot;color:rgb(128, 128, 128);text-align:center&quot;&gt;Violet Aesthetic Rule 7&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;padding:4px&quot; class=&quot;ant-col ant-col-xs-24 ant-col-sm-24 ant-col-lg-12 ant-col-xl-12&quot;&gt;&lt;div style=&quot;height:200px;border:1px solid rgba(128,128,128,0.2);border-radius:8px&quot;&gt;&lt;div style=&quot;height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center&quot;&gt;&lt;div style=&quot;width:100%;flex-grow:1;padding:16px&quot;&gt;&lt;/div&gt;&lt;p style=&quot;color:rgb(128, 128, 128);text-align:center&quot;&gt;Obey Aesthetic Rule 7&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;It happens when large neighbors surround small subtrees. The algorithm will pile the small ones to the left.&lt;/p&gt;&lt;p&gt;A simple fix is to take the average positions of the original layout and the mirror of the mirrored layout. But it tends to cluster the small subtrees at the center.&lt;/p&gt;&lt;div class=&quot;zxch3n-blog-container&quot;&gt;&lt;div class=&quot;ant-row&quot; style=&quot;width:100%;max-width:800px;display:flex;align-items:center;justify-content:center&quot;&gt;&lt;div style=&quot;padding:4px&quot; class=&quot;ant-col ant-col-xs-24 ant-col-sm-24 ant-col-lg-12 ant-col-xl-12&quot;&gt;&lt;div style=&quot;height:200px;border:1px solid rgba(128,128,128,0.2);border-radius:8px&quot;&gt;&lt;div style=&quot;height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center&quot;&gt;&lt;div style=&quot;width:100%;flex-grow:1;padding:16px&quot;&gt;&lt;/div&gt;&lt;p style=&quot;color:rgb(128, 128, 128);text-align:center&quot;&gt;Taking average positions&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Walker [&lt;a href=&quot;#ref3&quot;&gt;3&lt;/a&gt;] designed the first algorithm to address this issue, giving a more visually pleasing output. Its idea is that when moving a subtree to the right, the move distance should also be distributed to the smaller interior subtrees.&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:49.36708860759494%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABEUlEQVQoz22SiQ6EMAhE+/9fadR43/cV2TwSNtVdTK0CHYah7r5vMTvPU3wj5i/zvfN8c7yu65JpmiSKIun7XuZ5lm3bHqDYcRzStq3mkbOuq/r2fddvcBRwGAZJ01TyPJeiKCTLMqmq6oddWZbfOKvrOl34OE8RR3LTNBqgyrIsGoDpOI76b+woSCfELQ8ynGeHqTPtcHIAYJjwby37RhHYE6/rWiWgVevE+ULDwhZJHEZLADgIO/zEyYcRu19UW+Z5TxewOI51AEEQSBiGkiSJgtKeP2lfa/cW3qZO2+hkQ2A3H/rC8t/VegCSRIuwAAAmNgR2WKMx7QNMy38BrRIACI1mJji7XQ++fb9dLZ/pBzpVCSkiyKQ9AAAAAElFTkSuQmCC&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;How to fix aesthetic rule 7&quot; title=&quot;How to fix aesthetic rule 7&quot; src=&quot;/static/8169ae6ec013c2138ed6dd5352e17179/f058b/fix-a7.png&quot; srcSet=&quot;/static/8169ae6ec013c2138ed6dd5352e17179/c26ae/fix-a7.png 158w,/static/8169ae6ec013c2138ed6dd5352e17179/6bdcf/fix-a7.png 315w,/static/8169ae6ec013c2138ed6dd5352e17179/f058b/fix-a7.png 630w,/static/8169ae6ec013c2138ed6dd5352e17179/40601/fix-a7.png 945w,/static/8169ae6ec013c2138ed6dd5352e17179/4ad3a/fix-a7.png 1152w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;&lt;/p&gt;&lt;p&gt;To make the overall algorithm run within linear time, we need an O(1) method to&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Find the intermediate siblings&lt;/li&gt;&lt;li&gt;Distribute the distance evenly to the intermedia siblings&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Now the code changes to&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;layoutSubtree&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; children&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; prev &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; children&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cur &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; children&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; collideIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCollisionIndex&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; distance &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getMoveDistance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    cur&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;relativeX &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; distance
    &lt;span class=&quot;token function&quot;&gt;distributeDistanceToInteriorSubtrees&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; distance&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; collideIndex&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; i&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    prev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;positionRoot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&quot;the-on-layout-algorithm-5&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#the-on-layout-algorithm-5&quot; aria-label=&quot;the on layout algorithm 5 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The O(n) layout algorithm [&lt;a href=&quot;#ref5&quot;&gt;5&lt;/a&gt;]&lt;/h2&gt;&lt;p&gt;To make the overall algorithm run in O(n), we need the following methods done in &lt;span class=&quot;math math-inline&quot;&gt;O(n^{1-ϵ})&lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;md&quot;&gt;&lt;pre class=&quot;language-md&quot;&gt;&lt;code class=&quot;language-md&quot;&gt;&lt;span class=&quot;token list punctuation&quot;&gt;1.&lt;/span&gt;  getMoveDistance(leftSiblings, subtree)
&lt;span class=&quot;token list punctuation&quot;&gt;2.&lt;/span&gt;  getCollisionIndex(leftSiblings, subtree)
&lt;span class=&quot;token list punctuation&quot;&gt;3.&lt;/span&gt;  distributeDistanceToInteriorSubtrees(children, distance, collideIndex, currentIndex)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&quot;determine-move-distance&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#determine-move-distance&quot; aria-label=&quot;determine move distance permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Determine move distance&lt;/h3&gt;&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;getMoveDistance&lt;/code&gt; calculates how far a subtree needs to move to avoid colliding with the siblings on its left. For this purpose, it only needs to calculate the distance between the siblings’ right and the subtree’s left contour.&lt;/p&gt;&lt;p&gt;Note that aesthetic rule 5 requires: a subtree should be drawn the same way regardless of where it occurs in the tree. So a subtree’s left and right contour are not affected by other subtrees or their ancestors.&lt;/p&gt;&lt;p&gt;We introduce four new variables for this purpose.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;threadLeft: points to the next left contour node&lt;/li&gt;&lt;li&gt;modifierThreadLeft: the next left contour node x position relative to this&lt;/li&gt;&lt;li&gt;threadRight: points to the next right contour node&lt;/li&gt;&lt;li&gt;modifierThreadRight: the next right contour node x position relative to this&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The thread of the right contour is:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The first node of the thread is the rightmost subtree’s root.&lt;/li&gt;&lt;li&gt;If the current thread node has children, then the next node in the thread is its last-child&lt;/li&gt;&lt;li&gt;If the current thread node &lt;code class=&quot;language-text&quot;&gt;c&lt;/code&gt; has no child, the next node in the thread is &lt;code class=&quot;language-text&quot;&gt;c.threadRight&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;c.modifierThreadRight&lt;/code&gt; is &lt;code class=&quot;language-text&quot;&gt;c.threadRight&lt;/code&gt;’s x position relative to &lt;code class=&quot;language-text&quot;&gt;c&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The left contour thread follows the right contour’s mirrored rules.&lt;/p&gt;&lt;p&gt;When calculating the contour’s distance, only nodes with intersections on the y-axis need to be compared. And in both layered and non-layered cases, each node’s y positions can be precomputed.&lt;/p&gt;&lt;p&gt;We can express the extended behaviors in pseudocode.&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;layoutSubtree&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; children&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; prev &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; children&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cur &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; children&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; collideIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCollisionIndex&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; distance &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getMoveDistance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    cur&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;relativeX &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; distance&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;distributeDistanceToInteriorSubtrees&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; distance&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; collideIndex&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; i&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;   &lt;span class=&quot;token function&quot;&gt;mergeContour&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    prev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;positionRoot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getMoveDistance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; curLeftContour &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; prevRightContour &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; prev&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; maxDistance &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;  &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;curLeftContour &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; prevRightContour&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; xL &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getRelativeX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;curLeftContour&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; xR &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getRelativeX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prevRightContour&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; prevRightContour&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; margin&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    maxDistance &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;maxDistance&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; xR &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; xL&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; yL &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; curLeftContour&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; curLeftContour&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; yR &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; prevRightContour&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; prevRightContour&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;yL &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; yR&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;      curLeftContour &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;nextLeftContour&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;curLeftContour&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;yL &lt;span class=&quot;token operator&quot;&gt;&amp;gt;=&lt;/span&gt; yR&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;      prevRightContour &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;nextRightContour&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prevRightContour&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; maxDistance&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mergeContour&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bottom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bottom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; extremeRight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getRightThreadLastNode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    extremeRight&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;threadRight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getRightThreadNodeAtY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; extremeRight&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    extremeRight&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;modifierThreadRight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bottom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bottom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; extremeLeft &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getLeftThreadLastNode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    extremeLeft&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;threadLeft &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getLeftThreadNodeAtY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cur&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; extremeLeft&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    extremeLeft&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;modifierThreadLeft &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&quot;get-collided-subtree&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#get-collided-subtree&quot; aria-label=&quot;get collided subtree permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Get collided subtree&lt;/h3&gt;&lt;p&gt;During the post-order traversal, inside each iteration, we do layouts on subtrees that share the same parent.
&lt;code class=&quot;language-text&quot;&gt;getCollisionIndex&lt;/code&gt; get which subtree the right contour node, which produces the &lt;code class=&quot;language-text&quot;&gt;maxDistance&lt;/code&gt;, belongs to.&lt;/p&gt;&lt;p&gt;The easiest way to implement is to follow the parent pointer of the node to find the subtree’s root. But in the worst case, it takes O(n^2).&lt;/p&gt;&lt;p&gt;Notice that each subtree can only take a continuous y-span in the right contour thread of a forest. And the y-span always starts with another subtree’s bottom or 0 and ends with the bottom of the subtree.&lt;/p&gt;&lt;p&gt;So we can tell which subtree the right contour node belongs to by the y position.&lt;/p&gt;&lt;p&gt;Ploeg [&lt;a href=&quot;#ref5&quot;&gt;5&lt;/a&gt;] used a linked list to construct this data structure:&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token constant&quot;&gt;IYL&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;  &lt;span class=&quot;token function&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; bottom&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; index&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; next&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;IYL&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;  &lt;span class=&quot;token function&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;minY&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; index&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; cur &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cur &lt;span class=&quot;token operator&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; minY &lt;span class=&quot;token operator&quot;&gt;&amp;gt;=&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;bottom&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; cur &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;next&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token constant&quot;&gt;IYL&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;minY&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; index&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;layoutSubtree&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; children&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; prev &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; iyl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token constant&quot;&gt;IYL&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getBottom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; children&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cur &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; children&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;   &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;distance&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; collideIndex&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getMoveDistance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; iyl&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    cur&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;relativeX &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; distance&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;distributeDistanceToInteriorSubtrees&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; distance&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; collideIndex&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; i&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;mergeContour&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    prev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;   iyl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; iyl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getBottom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; i&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;positionRoot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getMoveDistance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; iyl&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; curLeftContour &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; prevRightContour &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; prev&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; maxDistance &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; collideIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;curLeftContour &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; prevRightContour&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;   &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;xR&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; xR&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height &lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; iyl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;bottom&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;     iyl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; iyl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;next&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; xL &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getRelativeX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;curLeftContour&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; xR &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getRelativeX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prevRightContour&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; prevRightContour&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; margin&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;   &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;xR &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; xL &lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; maxDistance&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;     maxDistance &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; xR &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; xL&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;     collideIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; iyl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; yL &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; curLeftContour&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; curLeftContour&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; yR &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; prevRightContour&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; prevRightContour&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;yL &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; yR&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      curLeftContour &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;nextLeftContour&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;curLeftContour&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;yL &lt;span class=&quot;token operator&quot;&gt;&amp;gt;=&lt;/span&gt; yR&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      prevRightContour &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;nextRightContour&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prevRightContour&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;maxDistance&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; collideIndex&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&quot;distribute-spacing&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#distribute-spacing&quot; aria-label=&quot;distribute spacing permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Distribute spacing&lt;/h3&gt;&lt;p&gt;We can implement it in a bruit-force way as below. But in the worst case, it causes the overall complexity to be &lt;span class=&quot;math math-inline&quot;&gt;O(n^2)&lt;/span&gt;.&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;distributeDistanceToInteriorSubtrees&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; distance&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; from&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; to&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; from &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; to&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    children&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;relativeX &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;distance &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; from&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;to &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; from&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;[&lt;a href=&quot;#ref4&quot;&gt;4&lt;/a&gt;] introduced a simple trick to make it O(1). Given that the distance distributed is always an arithmetic progression, we can cache the difference at the start node and clear the effect at the end node. We need two variables: &lt;code class=&quot;language-text&quot;&gt;shiftAcceleration&lt;/code&gt; and &lt;code class=&quot;language-text&quot;&gt;shiftChange&lt;/code&gt;. They are cached position changes that will apply to children[from+1..to] in &lt;code class=&quot;language-text&quot;&gt;finalizeAbsolutePosition&lt;/code&gt;.&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;layout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;root&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postOrderTraverse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// determine each child subtree&amp;#x27;s relative position to the parent&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;layoutSubtree&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;preOrderTraverse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;finalizeAbsolutePosition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;finalizeAbsolutePosition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;addChildSpacing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;addChildSpacing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; speed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; delta &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; child &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    speed &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; child&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;shiftAcceleration&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    delta &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; speed &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; child&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;shiftChange&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    child&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;relativeX &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; delta&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;distributeDistanceToInteriorSubtrees&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; distance&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; from&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; to&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;to &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; from &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  children&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;from &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;shiftAcceleration &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; distance&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;to&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;from&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  children&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;to&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;shiftAcceleration &lt;span class=&quot;token operator&quot;&gt;-=&lt;/span&gt; distance&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;to&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;from&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  children&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;to&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;shiftChange &lt;span class=&quot;token operator&quot;&gt;-=&lt;/span&gt; distance &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; distance&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;to&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;from&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&quot;final-code&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#final-code&quot; aria-label=&quot;final code permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Final Code&lt;/h2&gt;&lt;p&gt;You can find the final code of this article &lt;a href=&quot;https://gist.github.com/zxch3n/1f28317731876a8d9026d4d7d4eef892&quot;&gt;here&lt;/a&gt;. However, there are still a few trivial details missing in this abstraction. For details, please refer to the &lt;a href=&quot;https://github.com/zxch3n/tidy/blob/master/rust/crates/tidy-tree/src/layout/tidy_layout.rs&quot;&gt;source code&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;interactive-example&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#interactive-example&quot; aria-label=&quot;interactive example permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Interactive example&lt;/h2&gt;&lt;div class=&quot;zxch3n-blog-container&quot; style=&quot;margin:2em auto;display:flex;justify-content:center;align-items:center;width:100%&quot;&gt;&lt;div style=&quot;height:500px;padding:16px;box-shadow:0px 0px 8px rgba(128,128,128,0.3);display:flex;flex-direction:column;align-items:center;width:100%;max-width:600px&quot;&gt;&lt;div style=&quot;width:100%;height:100%&quot;&gt;&lt;/div&gt;&lt;div class=&quot;ant-row&quot; style=&quot;display:flex;margin-bottom:24px;width:100%&quot;&gt;&lt;div style=&quot;display:flex;justify-content:flex-end&quot; class=&quot;ant-col ant-col-xs-24 ant-col-sm-24 ant-col-md-12 ant-col-lg-12&quot;&gt;&lt;div style=&quot;display:flex;align-items:center;width:140px;padding:4px;text-align:right&quot;&gt;Layout Type:&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ant-col ant-col-xs-24 ant-col-sm-24 ant-col-md-12 ant-col-lg-12&quot;&gt;&lt;div class=&quot;ant-select ant-select-single ant-select-show-arrow&quot; style=&quot;width:150px&quot;&gt;&lt;div class=&quot;ant-select-selector&quot;&gt;&lt;span class=&quot;ant-select-selection-search&quot;&gt;&lt;input type=&quot;search&quot; autoComplete=&quot;off&quot; class=&quot;ant-select-selection-search-input&quot; role=&quot;combobox&quot; aria-haspopup=&quot;listbox&quot; aria-owns=&quot;undefined_list&quot; aria-autocomplete=&quot;list&quot; aria-controls=&quot;undefined_list&quot; aria-activedescendant=&quot;undefined_list_0&quot; value=&quot;&quot; readonly=&quot;&quot; unselectable=&quot;on&quot; style=&quot;opacity:0&quot;/&gt;&lt;/span&gt;&lt;span class=&quot;ant-select-selection-item&quot; title=&quot;Tidy&quot;&gt;Tidy&lt;/span&gt;&lt;/div&gt;&lt;span class=&quot;ant-select-arrow&quot; style=&quot;user-select:none;-webkit-user-select:none&quot; unselectable=&quot;on&quot; aria-hidden=&quot;true&quot;&gt;&lt;span role=&quot;img&quot; aria-label=&quot;down&quot; class=&quot;anticon anticon-down ant-select-suffix&quot;&gt;&lt;svg viewBox=&quot;64 64 896 896&quot; focusable=&quot;false&quot; data-icon=&quot;down&quot; width=&quot;1em&quot; height=&quot;1em&quot; fill=&quot;currentColor&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ant-row&quot; style=&quot;display:flex;width:100%;max-width:600px&quot;&gt;&lt;div style=&quot;display:flex;justify-content:flex-end&quot; class=&quot;ant-col ant-col-xs-24 ant-col-sm-24 ant-col-md-12 ant-col-lg-12&quot;&gt;&lt;div class=&quot;ant-input-number-group-wrapper&quot; style=&quot;width:140px;margin-right:20px&quot;&gt;&lt;div class=&quot;ant-input-number-wrapper ant-input-number-group&quot;&gt;&lt;div class=&quot;ant-input-number&quot;&gt;&lt;div class=&quot;ant-input-number-handler-wrap&quot;&gt;&lt;span unselectable=&quot;on&quot; role=&quot;button&quot; aria-label=&quot;Increase Value&quot; aria-disabled=&quot;false&quot; class=&quot;ant-input-number-handler ant-input-number-handler-up&quot;&gt;&lt;span role=&quot;img&quot; aria-label=&quot;up&quot; class=&quot;anticon anticon-up ant-input-number-handler-up-inner&quot;&gt;&lt;svg viewBox=&quot;64 64 896 896&quot; focusable=&quot;false&quot; data-icon=&quot;up&quot; width=&quot;1em&quot; height=&quot;1em&quot; fill=&quot;currentColor&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/span&gt;&lt;span unselectable=&quot;on&quot; role=&quot;button&quot; aria-label=&quot;Decrease Value&quot; aria-disabled=&quot;false&quot; class=&quot;ant-input-number-handler ant-input-number-handler-down&quot;&gt;&lt;span role=&quot;img&quot; aria-label=&quot;down&quot; class=&quot;anticon anticon-down ant-input-number-handler-down-inner&quot;&gt;&lt;svg viewBox=&quot;64 64 896 896&quot; focusable=&quot;false&quot; data-icon=&quot;down&quot; width=&quot;1em&quot; height=&quot;1em&quot; fill=&quot;currentColor&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;ant-input-number-input-wrap&quot;&gt;&lt;input autoComplete=&quot;off&quot; role=&quot;spinbutton&quot; aria-valuemin=&quot;1&quot; aria-valuemax=&quot;500&quot; aria-valuenow=&quot;50&quot; step=&quot;1&quot; class=&quot;ant-input-number-input&quot; value=&quot;50&quot;/&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ant-input-number-group-addon&quot;&gt;Nodes&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ant-col ant-col-xs-24 ant-col-sm-24 ant-col-md-12 ant-col-lg-12&quot;&gt;&lt;div class=&quot;ant-slider ant-slider-horizontal&quot; style=&quot;width:200px&quot;&gt;&lt;div class=&quot;ant-slider-rail&quot;&gt;&lt;/div&gt;&lt;div class=&quot;ant-slider-track&quot; style=&quot;left:0%;width:9.819639278557114%&quot;&gt;&lt;/div&gt;&lt;div class=&quot;ant-slider-step&quot;&gt;&lt;/div&gt;&lt;div class=&quot;ant-slider-handle&quot; style=&quot;left:9.819639278557114%;transform:translateX(-50%)&quot; tabindex=&quot;0&quot; role=&quot;slider&quot; aria-valuemin=&quot;1&quot; aria-valuemax=&quot;500&quot; aria-valuenow=&quot;50&quot; aria-disabled=&quot;false&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;In this example, you can try non-layered tidy layout, layered tidy layout, and naive layout.&lt;/p&gt;&lt;h1 id=&quot;relayout-partially&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#relayout-partially&quot; aria-label=&quot;relayout partially permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Relayout partially&lt;/h1&gt;&lt;p&gt;Node editing is a common scenario in applications like mind maps. If the relayout time is larger than 16ms, there will be obvious freezes, which is unacceptable. Therefore, a partial relayout is required for a smooth user experience on large editable tree visualization.&lt;/p&gt;&lt;p&gt;In this section, we exclude the &lt;code class=&quot;language-text&quot;&gt;finalizeAbsolutePosition&lt;/code&gt; and only talk about how to re-calculate each node’s relative position to their parent. Because changing one node’s size may cause the entire tree’s absolute positions to change, relative positions are much more stable as &lt;a title=&quot;A subtree should be drawn the same way regardless of where it occurs in the tree&quot; href=&quot;#aesthetic-rules&quot;&gt;aesthetic rule 5&lt;/a&gt; required. And in the real-world application, we only need the absolute positions of the nodes inside the screen. By detecting the collision between trees’ bounding boxes and the screen, we can filter out the offscreen content swiftly. Strictly speaking, the time complexity of partial relayout is O(d + m), where m is the in-screen nodes number, and d is the max depth of changed nodes.&lt;/p&gt;&lt;p&gt;Adding the partially relayout support to the naive version is relatively easy. Because the affected states are straightforward to reason about, i.e., only the bounding boxes of the edited nodes and their ancestors are changed.&lt;/p&gt;&lt;p&gt;For partial relayout, we need to determine which thread pointer caches are outdated. We say a subtree is changed if it or its offspring’s sizes change or an insertion or deletion happens inside this subtree. Note that&lt;/p&gt;&lt;ol&gt;&lt;li&gt;If all nodes inside a subtree are not changed, the thread pointers of its nodes that point to the node inside this subtree won’t change. This is because the layout of a tree is agnostic about nodes outside. And the thread pointers that point inside are only affected by the structure of the subtree itself.&lt;/li&gt;&lt;li&gt;In a subtree, only the deepest nodes, which have the greatest value of &lt;code class=&quot;language-text&quot;&gt;node.y + node.height&lt;/code&gt;, have thread pointers that point outside of the tree. This rule is obvious in the &lt;code class=&quot;language-text&quot;&gt;mergeContour&lt;/code&gt; implementation.&lt;/li&gt;&lt;li&gt;Based on 1 and 2, we can infer that we only need to update the thread pointers of its deepest nodes for an unchanged subtree.&lt;/li&gt;&lt;li&gt;From 3, we can infer that if a node and its parent are roots of unchanged subtrees, we only need to update the parent subtree’s deepest nodes’ threads.&lt;/li&gt;&lt;li&gt;Generalizing 4, for all unchanged subtrees, only those who are siblings of the changed subtrees need to update their deepest nodes’ thread pointers.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Based on the above observations, we only need to relayout the changed subtree and update its siblings’ thread pointers. Below is the code about partial relayout when there is a single changed node. We can easily extend it to multiple changed nodes.&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;relayout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;root&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; changedNode&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; node &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; changedNode
  &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sibling &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; r &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getRightBottomNode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sibling&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;threadRight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
      r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;modifierThreadRight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; l &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getLeftBottomtNode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sibling&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      l&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;threadRight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
      l&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;modifierThreadRight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token function&quot;&gt;layoutSubtree&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    node &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parent
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id=&quot;benchmark&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#benchmark&quot; aria-label=&quot;benchmark permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Benchmark&lt;/h1&gt;&lt;p&gt;The code is available at &lt;a href=&quot;https://github.com/zxch3n/tidy&quot;&gt;GitHub&lt;/a&gt;. The layout algorithms are written in Rust and compiled to WASM. The renderer is written in TypeScript.&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:52.53164556962025%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAABsUlEQVQoz5WT2W7bMBBF9QENlyEpLhK1eYlt9aGO0yBCWvQPij72/3/kFqRo2E2BAnk4GIAjDu/MXFXjOCHhvYcQAlLKD3N/rwohoGkaWGvXhBA5/g8pBbi4QSTBGMu5SmsNzvn6IVF5id4hS26FSQmnFbxWiMag7yf0fQ9jDKqkTpICFdLFNd7OiAiCVCadz8Fj6TwaLUDiAV3s8LosmKYNKuf9+vq7gvcISfDaZFXRCHSGQ2uLzbBg9DNMbXMx7wOqkBXSX2pIrUpYadcpgZNX6KzHxn/GsX3DMv/G18MvHMYXMPYpzzCNrkrLuG+TSwkmBJTk2NYaWx+xaXYY4zP6eMH56ScO5x+I318gjxFy10CK28ZzQZG3xvP2UmuTi/gyPeG8f8M0XtBvLojTDNNF0KkD7VqQr6GsXUdytY2QqFwIcC5iiHucHy94Pn7D8fEVcZhBvoGwBow4OBUXMLkiCpKK3YpCbS220wm7acbQ7VG7gDo4mNqgbRr0/YCu6/PAnQ+onYNPvk3Rh39+hkpwDsYfwHky5to2FT+2bYthGLLHVFpUuXTNZzuVglfT/wFH+hXgSsZ/7wAAAABJRU5ErkJggg==&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;benchmark&quot; title=&quot;benchmark&quot; src=&quot;/static/ac9d4951191ec1fbc601ff7907727e27/f058b/benchmark.png&quot; srcSet=&quot;/static/ac9d4951191ec1fbc601ff7907727e27/c26ae/benchmark.png 158w,/static/ac9d4951191ec1fbc601ff7907727e27/6bdcf/benchmark.png 315w,/static/ac9d4951191ec1fbc601ff7907727e27/f058b/benchmark.png 630w,/static/ac9d4951191ec1fbc601ff7907727e27/40601/benchmark.png 945w,/static/ac9d4951191ec1fbc601ff7907727e27/78612/benchmark.png 1260w,/static/ac9d4951191ec1fbc601ff7907727e27/0faf1/benchmark.png 1956w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;&lt;/p&gt;&lt;div class=&quot;zxch3n-blog-container&quot; style=&quot;display:flex;justify-content:center;align-content:center;flex-direction:column&quot;&gt;&lt;button style=&quot;margin-top:16px&quot; type=&quot;button&quot; class=&quot;ant-btn ant-btn-default&quot;&gt;&lt;span&gt;Run Benchmark On Your Device&lt;/span&gt;&lt;/button&gt;&lt;/div&gt;&lt;p&gt;Benchmarks were done on MacBook Pro (13-inch, M1, 2020). It only measures the layout algorithm without the allocation and deallocation time. Mysteriously, the WASM build is faster than the native build on large data. It is very interesting, but I have not found out why.&lt;/p&gt;&lt;h1 id=&quot;future-work&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#future-work&quot; aria-label=&quot;future work permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Future work&lt;/h1&gt;&lt;h2 id=&quot;remaining-aesthetic-issue&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#remaining-aesthetic-issue&quot; aria-label=&quot;remaining aesthetic issue permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Remaining aesthetic issue&lt;/h2&gt;&lt;p&gt;The current layout algorithm is still not ideal in some cases.&lt;/p&gt;&lt;div class=&quot;zxch3n-blog-container&quot;&gt;&lt;div class=&quot;ant-row&quot; style=&quot;max-width:1200px;width:100%&quot;&gt;&lt;div style=&quot;padding:4px&quot; class=&quot;ant-col ant-col-xs-24 ant-col-sm-24 ant-col-lg-12 ant-col-xl-12&quot;&gt;&lt;div style=&quot;height:200px;border:1px solid rgba(128,128,128,0.2);border-radius:8px;padding:12px&quot;&gt;&lt;div style=&quot;height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center&quot;&gt;&lt;div style=&quot;width:100%;height:100%;flex-grow:1&quot;&gt;&lt;/div&gt;&lt;p style=&quot;color:rgb(128, 128, 128);text-align:center;padding-bottom:8px&quot;&gt;Current layout&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;padding:4px&quot; class=&quot;ant-col ant-col-xs-24 ant-col-sm-24 ant-col-lg-12 ant-col-xl-12&quot;&gt;&lt;div style=&quot;height:200px;border:1px solid rgba(128,128,128,0.2);border-radius:8px&quot;&gt;&lt;div style=&quot;height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center&quot;&gt;&lt;div style=&quot;width:100%;flex-grow:1;padding:16px&quot;&gt;&lt;/div&gt;&lt;p style=&quot;color:rgb(128, 128, 128);text-align:center;padding-bottom:8px&quot;&gt;Ideal layout&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;As the above example shows, the root’s children are not symmetric in the current layout though it obeys the aesthetic rules. It does not violate &lt;a title=&quot;A tree and its mirror image should produce drawings that are reflections of one another, which implies&quot; href=&quot;#aesthetic-rules&quot;&gt;aesthetic rule 7&lt;/a&gt;, since the drawing of mirrored structure would give a mirrored layout.&lt;/p&gt;&lt;p&gt;To fix this issue, we need a new aesthetic rule to formalize the problem.&lt;/p&gt;&lt;h2 id=&quot;the-tidy-library&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#the-tidy-library&quot; aria-label=&quot;the tidy library permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The Tidy Library&lt;/h2&gt;&lt;p&gt;Though tidy tree visualization is useful in many fields, we haven’t seen many adoptions of this algorithm in the industry
because it is error-prone and slower. I hope &lt;a href=&quot;https://github.com/zxch3n/tidy&quot;&gt;Tidy Lib&lt;/a&gt; can make the adoption simple and reliable.&lt;/p&gt;&lt;p&gt;The plan is to add full support for partial relayout, compress wasm size, and performance improvement.
This lib mainly focuses on the layout algorithm. Building a full-fledged tree visualization tool or editor is not a priority of this lib.&lt;/p&gt;&lt;p&gt;This lib is published under MIT License. Don’t hesitate to contact me if you want to participate or have any suggestions.&lt;/p&gt;&lt;h1 id=&quot;references&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#references&quot; aria-label=&quot;references permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;References&lt;/h1&gt;&lt;ul&gt;&lt;li id=&quot;ref1&quot;&gt;[1]C. Wetherell and A. Shannon, “Tidy Drawings of Trees,” IEEE Transactions on Software Engineering, vol. SE-5, no. 5, pp. 514–520, Sep. 1979, doi: 10.1109/TSE.1979.234212.&lt;/li&gt;&lt;li id=&quot;ref2&quot;&gt;[2] E. M. Reingold and J. S. Tilford, “Tidier Drawings of Trees,” IEEE Transactions on Software Engineering, vol. SE-7, no. 2, pp. 223–228, Mar. 1981, doi: 10.1109/TSE.1981.234519.&lt;/li&gt;&lt;li id=&quot;ref3&quot;&gt;[3] J. Q. Walker II, “A node-positioning algorithm for general trees,” Software: Practice and Experience, vol. 20, no. 7, pp. 685–705, 1990, doi: 10.1002/spe.4380200705.&lt;/li&gt;&lt;li id=&quot;ref4&quot;&gt;[4] C. Buchheim, M. Jünger, and S. Leipert, “Improving Walker’s Algorithm to Run in Linear Time,” in Graph Drawing, Berlin, Heidelberg, 2002, pp. 344–353. doi: 10.1007/3-540-36151-0_32.&lt;/li&gt;&lt;li id=&quot;ref5&quot;&gt;[5] A. van der Ploeg, “Drawing non-layered tidy trees in linear time,” Software: Practice and Experience, vol. 44, no. 12, pp. 1467–1484, 2014, doi: 10.1002/spe.2213.&lt;/li&gt;&lt;li id=&quot;ref6&quot;&gt;[6] Bill Mill, “Drawing Presentable Trees.” https://llimllib.github.io/pymag-trees/ 2008&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[如何设计 CRDT 算法]]></title><description><![CDATA[   Marc Shapiro 在论文  Conflict-free replicated data types  中正式定义了两种 CRDT 方法，基于操作的(Operation-based) 和基于状态的(State-based…]]></description><link>https://www.zxch3n.com/crdt-intro/design-crdt/</link><guid isPermaLink="false">https://www.zxch3n.com/crdt-intro/design-crdt/</guid><pubDate>Thu, 23 Dec 2021 14:00:02 GMT</pubDate><content:encoded>&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:58.86075949367089%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABRNAAAUTQGUyo0vAAAByUlEQVQoz41Ta1PaQBTl//+JfnE6HduxVqfqOLZWRRGwYHgFaEhIIAbyBJJATLI5nb2WoIzTYWc2u7mz95x77p4tAECWZTS394xlYIzl8V1GgX+WQYS5F1LANue0uvYCoqBiKE2wWj6/IfvfJMDQX8GazBAGETTZRJKkkPsGHssS6vcSFrMwr4ClLE9+s2eMFL0ABhFc26eE8dCiA8swQrehQRQ0iqdJijRlu0mO44SATMOD2NAIMFrFaNUUCFWZ5I9VC4d7tyhddnB1JqBy08XdzzYujmooXrRQK/2BOpi+AHL2XmuEdn2I5oMMf74kUNOYQRJ1PEcxya6XJVSLPTQeZNz+aOPzh2s0fyvot8f4dSoQMAE61gIzN8D1eRNCZUDV8H7oqg2lb7wrjatK4jT/533PL8VzfKru+FMJ3z7eQZWmCPwVSdI1O0/iBLy6kWLi5EsFX/eKlHN2UIXcMzY95AenTx6+75dJ1vAfIL9lLn89jLGDjqDCcwJ0myPqWedRxZPmUM9zQM441V1MdBcjxUL5SoRjzsmTXPquxt74MIioZG5koTog5m3DrhM4wWvf8TVjmzOF9YZ7jL+I143eZt9+nu+R/gVzCpDXGJkojAAAAABJRU5ErkJggg==&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;设计 CRDT&quot; title=&quot;设计 CRDT&quot; src=&quot;/static/4e78d08dad64b835622765444af0c517/f058b/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png&quot; srcSet=&quot;/static/4e78d08dad64b835622765444af0c517/c26ae/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 158w,/static/4e78d08dad64b835622765444af0c517/6bdcf/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 315w,/static/4e78d08dad64b835622765444af0c517/f058b/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 630w,/static/4e78d08dad64b835622765444af0c517/40601/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 945w,/static/4e78d08dad64b835622765444af0c517/78612/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 1260w,/static/4e78d08dad64b835622765444af0c517/4e814/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 3211w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;  &lt;/p&gt;&lt;p&gt;Marc Shapiro 在论文 &lt;a href=&quot;https://readpaper.com/paper/1516319412&quot;&gt;Conflict-free replicated data types&lt;/a&gt; 中正式定义了两种 CRDT 方法，基于操作的(Operation-based) 和基于状态的(State-based)，以及他们的&lt;u&gt;&lt;strong&gt;充分条件&lt;/strong&gt;&lt;/u&gt;。从而依据充分条件就可以更轻松明确地设计出 CRDT 算法。下文将介绍这些充分条件&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;PS: 为了方便理解对原文进行了简化，严谨的推导和证明见 &lt;a href=&quot;https://kirgizov.link/teaching/esirem/information-systems-2021/CRDT/CRDT.pdf&quot;&gt;CRDT 原文&lt;/a&gt;。&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#%E6%9C%AF%E8%AF%AD--%E4%B8%80%E4%BA%9B%E5%AE%9A%E4%B9%89&quot;&gt;术语 &amp;amp; 一些定义&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#%E6%9C%80%E7%BB%88%E4%B8%80%E8%87%B4%E6%80%A7---eventual-consistency&quot;&gt;最终一致性 - Eventual Consistency&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#-%E5%BC%BA%E6%9C%80%E7%BB%88%E4%B8%80%E8%87%B4%E6%80%A7---strong-eventual-consistency-sec&quot;&gt;💪 强最终一致性 - Strong Eventual Consistency (SEC)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#crdt&quot;&gt;CRDT&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%81%8F%E5%BA%8F%E9%9B%86---partial-order&quot;&gt;偏序集 - Partial Order&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%8D%8A%E6%A0%BC---semilatice&quot;&gt;半格 - Semilatice&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E4%B8%A4%E7%A7%8D-crdt-%E7%B1%BB%E5%9E%8B&quot;&gt;两种 CRDT 类型&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#%E5%9F%BA%E4%BA%8E%E7%8A%B6%E6%80%81%E7%9A%84-crdt-state-based-crdt-cvrdt&quot;&gt;基于状态的 CRDT (State-based CRDT, CvRDT)&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#%E5%8D%95%E8%B0%83%E5%8D%8A%E6%A0%BC%E5%AF%B9%E8%B1%A1---monotonic-semilattice-object&quot;&gt;单调半格对象 - Monotonic Semilattice Object&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%AE%9A%E7%90%86&quot;&gt;定理&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1-state-based-crdt&quot;&gt;如何设计 State-based CRDT&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%9F%BA%E4%BA%8E%E6%93%8D%E4%BD%9C%E7%9A%84-crdtop-based-crdt-cmrdt&quot;&gt;基于操作的 CRDT（Op-based CRDT, CmRDT）&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#%E5%B9%B6%E8%A1%8C%E7%9A%84%E6%93%8D%E4%BD%9C---concurrent-operation&quot;&gt;并行的操作 - Concurrent Operation&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%AE%9A%E7%90%86-1&quot;&gt;定理&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1-op-based-crdt&quot;&gt;如何设计 Op-based CRDT&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#cmrdt-%E5%92%8C-cvrdt-%E6%98%AF%E7%AD%89%E4%BB%B7%E7%9A%84&quot;&gt;CmRDT 和 CvRDT 是等价的&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E8%AE%BE%E8%AE%A1-crdt&quot;&gt;设计 CRDT&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AA-state-based-crdt&quot;&gt;&lt;em&gt;如何设计一个 State-based CRDT&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AA-op-based-crdt&quot;&gt;&lt;em&gt;如何设计一个 Op-based CRDT&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AA%E5%8F%AF%E5%A2%9E%E5%8A%A0%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0%E7%9A%84-set-crdt&quot;&gt;设计一个可增加删除元素的 Set CRDT&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#lamport-timestamp&quot;&gt;Lamport Timestamp&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%AE%9E%E7%8E%B0&quot;&gt;实现&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;术语--一些定义&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E6%9C%AF%E8%AF%AD--%E4%B8%80%E4%BA%9B%E5%AE%9A%E4%B9%89&quot; aria-label=&quot;术语  一些定义 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;术语 &amp;amp; 一些定义&lt;/h1&gt;&lt;p&gt;为了深入 CRDT 的设计原则，我们需要先清晰定义问题。&lt;/p&gt;&lt;p&gt;我们考虑这样一个异步网络系统：系统中有多个副本(replica)，每个副本代表一个进程/一个计算机，副本可能会崩溃，我们用正确的副本指未崩溃的副本。一个副本可能会崩溃之后永不恢复，或者保持内存完整性的情况下地恢复。副本之间可能会被分区。&lt;/p&gt;&lt;h3 id=&quot;最终一致性---eventual-consistency&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E6%9C%80%E7%BB%88%E4%B8%80%E8%87%B4%E6%80%A7---eventual-consistency&quot; aria-label=&quot;最终一致性   eventual consistency permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;最终一致性 - Eventual Consistency&lt;/h3&gt;&lt;p&gt;最终一致性由三个属性构成&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Eventual Delivery: 发布到一个正确的副本上的更新列表最终将被传达至所有正确的副本&lt;/li&gt;&lt;li&gt;收敛 Convergence: 收到了一样的更新列表的正确副本们最终状态将一致&lt;/li&gt;&lt;li&gt;终止 Termination: 所有执行的方法都会终止（即保证算法能在有限时间内完成计算，而不是要进行 &lt;span class=&quot;math math-inline&quot;&gt;2^{128}&lt;/span&gt; 次计算来遍历整个状态空间的这种算法）&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:59.49367088607595%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAABU0lEQVQoz6WR224bMQxE/f9/1z6kBZrAbuzYWe9WV95ESdzCVuI6QJ5SvonAiGdmNmbWWlu/NBum8vryp9VmZrV2EU0RzcwtSbXlSMGDlhpcjgEh0fnkzN7FKeCP77vosbUWPUCm+dXniCkgU2EqkKlq2+/ObkmEZZnCv8taao7Uu91WqhWB7/FENCf6BLu3LlzuV2YWPQjreLbWL0pbB63ZnRiBt79eCLj3TihMZZ5C9PC8nXIkpvL76YTAbknHwyJc9rsJMt88w+PPA17FCFJEw+VsKaIjsNNhKaKEMk9BpeaEVd/a2Vyqqh+qEtbh2cxywt772EPm+2jeAgs+rx+Wa05Ua2MqI47hU0RvwO/YER++baOD3jpkFtZrPXzcz9HBAL64YGWUFPD4vAQP48cNoZxPrrVetSFwEZ0nL1yCgxxxcBXRa+1StUImvAW2/sf8BU8ou9ZpZHh3AAAAAElFTkSuQmCC&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;最终一致性示意图&quot; title=&quot;最终一致性示意图&quot; src=&quot;/static/d4c21a4ee0e794f667027c3aaa3f458e/f058b/e31293ba06158e4a864093eb2430c7501a7891dbb89784c2c82c3aa0a795ab1f.png&quot; srcSet=&quot;/static/d4c21a4ee0e794f667027c3aaa3f458e/c26ae/e31293ba06158e4a864093eb2430c7501a7891dbb89784c2c82c3aa0a795ab1f.png 158w,/static/d4c21a4ee0e794f667027c3aaa3f458e/6bdcf/e31293ba06158e4a864093eb2430c7501a7891dbb89784c2c82c3aa0a795ab1f.png 315w,/static/d4c21a4ee0e794f667027c3aaa3f458e/f058b/e31293ba06158e4a864093eb2430c7501a7891dbb89784c2c82c3aa0a795ab1f.png 630w,/static/d4c21a4ee0e794f667027c3aaa3f458e/40601/e31293ba06158e4a864093eb2430c7501a7891dbb89784c2c82c3aa0a795ab1f.png 945w,/static/d4c21a4ee0e794f667027c3aaa3f458e/78612/e31293ba06158e4a864093eb2430c7501a7891dbb89784c2c82c3aa0a795ab1f.png 1260w,/static/d4c21a4ee0e794f667027c3aaa3f458e/a076e/e31293ba06158e4a864093eb2430c7501a7891dbb89784c2c82c3aa0a795ab1f.png 1825w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;  &lt;/p&gt;&lt;h3 id=&quot;-强最终一致性---strong-eventual-consistency-sec&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#-%E5%BC%BA%E6%9C%80%E7%BB%88%E4%B8%80%E8%87%B4%E6%80%A7---strong-eventual-consistency-sec&quot; aria-label=&quot; 强最终一致性   strong eventual consistency sec permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;💪 强最终一致性 - Strong Eventual Consistency (SEC)&lt;/h3&gt;&lt;p&gt;强最终一致性定义为：满足「最终一致性 Eventual Consistency」且具有「强收敛性 Strong Convergence」。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;强收敛 Strong Convergence: 收到了一样的更新列表的正确副本们的状态一致&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/29381442/eventual-consistency-vs-strong-eventual-consistency-vs-strong-consistency&quot;&gt;「强最终一致性 SEC」和「最终一致性 EC」的区别&lt;/a&gt;在于 EC 有可能要求用户解决冲突，而 SEC 是不会发生冲突的。&lt;/p&gt;&lt;h3 id=&quot;crdt&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#crdt&quot; aria-label=&quot;crdt permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CRDT&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;CRDT 的定义为：满足「强最终一致性」的数据类型。&lt;/strong&gt;&lt;/p&gt;&lt;h3 id=&quot;偏序集---partial-order&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%81%8F%E5%BA%8F%E9%9B%86---partial-order&quot; aria-label=&quot;偏序集   partial order permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;偏序集 - Partial Order&lt;/h3&gt;&lt;p&gt;给定集合S，“≤”是S上的二元关系，若“≤”满足：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;自反性：∀a∈S，有a≤a；&lt;/li&gt;&lt;li&gt;反对称性：∀a，b∈S，a≤b且b≤a，则a=b；&lt;/li&gt;&lt;li&gt;传递性：∀a，b，c∈S，a≤b且b≤c，则a≤c；&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;则称集合 S 为偏序集&lt;/p&gt;&lt;p&gt;例如下图是以包含关系定义的集合上的偏序，A -&amp;gt; B 的箭头表示 A ≤ B。而 &lt;code class=&quot;language-text&quot;&gt;{x}&lt;/code&gt; 和 &lt;code class=&quot;language-text&quot;&gt;{y}&lt;/code&gt; 二者之间是不可比较的。&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:500px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:75.9493670886076%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAIAAABr+ngCAAAACXBIWXMAAAsTAAALEwEAmpwYAAABlklEQVQoz4VTa2+jMBDk//+7KIoECQIEcmG9fvMIBGxXZStKr9e7+bbrHe14PE7iLwAAvkMIEWMMIfycSc5F2BFjfL1el8sly7I0Ta/X63H6LzJhnmdENMZ0XaeUklIqpdZ1/WPNF5mKdV2NMc45732MUWu9LMvRt9Z67w91n2RjDOccAIQQxwbnXLvj+XxSZ1mWruuEEADQ9/0H2XvPGEvTNMsyRDyEFEWR5/ntdmvb9pBWlmWe54/Hg5oJ2VNV1duOaZq2bbPWMsb6vjfGMMaGYdi2jbTUdc0Y+3ZnKeW2bSEEznlZls65s71SyqIotNYxRmvtMAx0+kEexxEAyNh5nmOMfd8jotZaKSWEmKaJxoQQiCiEICOTdV0BoCiKpmkoDzFG7z3n/H6/N01jjDlUAECe52VZkorPzUopznld17SZXruqqq7ryP8QwjAMVVWpHTSWnLPlvXfOGWOIgIh0vXmetdbjOJKob+9MxTmbWmtrLSJaa494/Cfb56ghIgC0bauU+u3z/CXbxCdvpZRHwn6S3wHhGGDcdqnqIAAAAABJRU5ErkJggg==&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;集合上的偏序示意&quot; title=&quot;集合上的偏序示意&quot; src=&quot;/static/f7de6703fd33992bd7419d3296c6c259/0b533/d8b13702eb4486e250351e2fbc20502660e598d51103dea5887d7a84e3182e19.png&quot; srcSet=&quot;/static/f7de6703fd33992bd7419d3296c6c259/c26ae/d8b13702eb4486e250351e2fbc20502660e598d51103dea5887d7a84e3182e19.png 158w,/static/f7de6703fd33992bd7419d3296c6c259/6bdcf/d8b13702eb4486e250351e2fbc20502660e598d51103dea5887d7a84e3182e19.png 315w,/static/f7de6703fd33992bd7419d3296c6c259/0b533/d8b13702eb4486e250351e2fbc20502660e598d51103dea5887d7a84e3182e19.png 500w&quot; sizes=&quot;(max-width: 500px) 100vw, 500px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;  &lt;/p&gt;&lt;h3 id=&quot;半格---semilatice&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%8D%8A%E6%A0%BC---semilatice&quot; aria-label=&quot;半格   semilatice permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;半格 - Semilatice&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E5%8D%8A%E6%A0%BC&quot;&gt;半格&lt;/a&gt;是一个偏序集&lt;/li&gt;&lt;li&gt;每个非空集合都有上确界或者下确界&lt;ul&gt;&lt;li&gt;上确界被称为 Least upper bound (LUB)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;两种-crdt-类型&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E4%B8%A4%E7%A7%8D-crdt-%E7%B1%BB%E5%9E%8B&quot; aria-label=&quot;两种 crdt 类型 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;两种 CRDT 类型&lt;/h1&gt;&lt;h2 id=&quot;基于状态的-crdt-state-based-crdt-cvrdt&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%9F%BA%E4%BA%8E%E7%8A%B6%E6%80%81%E7%9A%84-crdt-state-based-crdt-cvrdt&quot; aria-label=&quot;基于状态的 crdt state based crdt cvrdt permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;基于状态的 CRDT (State-based CRDT, CvRDT)&lt;/h2&gt;&lt;p&gt;设计一个基于状态的 CRDT 中需要先定义一个 State-based Object。当它满足一定的性质时就能成为 State-based CRDT 👏。State-based Object 的定义包括：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;State&lt;/code&gt; 副本的内部状态的类型&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;state_zero&lt;/code&gt; 内部状态的初始值&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;≤&lt;/code&gt; 定义了 state 之间的顺序&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;update(s, u)&lt;/code&gt; 定义 state 的更新方式&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;merge(s, s&amp;#x27;)&lt;/code&gt; 函数可以用于合并两个状态得到新状态&lt;/li&gt;&lt;li&gt;副本之间通过传递自己的 state，并进行 merge 操作来达到一致性&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;单调半格对象---monotonic-semilattice-object&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%8D%95%E8%B0%83%E5%8D%8A%E6%A0%BC%E5%AF%B9%E8%B1%A1---monotonic-semilattice-object&quot; aria-label=&quot;单调半格对象   monotonic semilattice object permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;单调半格对象 - Monotonic Semilattice Object&lt;/h3&gt;&lt;p&gt;指一个 Stated-base Object，如果它满足以下性质就称其为单调半格&lt;/p&gt;&lt;ul&gt;&lt;li&gt;该对象集合是一个以 ≤ 为顺序的半格&lt;/li&gt;&lt;li&gt;合并「本地状态 s」和「远端状态 s’」的方式为计算二者的上确界（LUB），即 merge(s, s’) = &lt;span class=&quot;math math-inline&quot;&gt;s \sqcup s&amp;#x27;&lt;/span&gt;&lt;/li&gt;&lt;li&gt;在所有的更新中，s 都是单调递增的，即 s ≤ update(s, u) &lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:53.79746835443038%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABRNAAAUTQGUyo0vAAABYklEQVQoz4VSa3ODIBDk//+7fsqHtM3DPHxEBRREwFxnz2C102lvJhE49m53OUGveD6ftF5Pcdrup+98iqbSNDq/wQtcTJtpmijGidzgqbzLpWjfDVQVitd+DBRC5O8tqym/NoxJRcXpo2AwQjY9qdZQcWvJdAPv80tDVS7pen5wrtcDZYeKjvuc2R32OZ8vBb2fO84MZ6bjGCgx77TlvHOeRhf4bLAj3+uUpffddSv5Nw9TQNbPc7DBOQKFUXRT0Bq3dGQPQ2QppncMTJ4CGHxktvAUmLrUrCAVZMm7t9NCG/6otmfA/dLwXivLTWD+o1DcqK40lfeW2rqj7FhtXlqga3x5uA4wSNIAOH+Ws6+rqUAzPOA6BJKQwXJ8JO/jIjWNg5Z2mQT/uoecloZk3fMaGDQW+EMCHqHj8tWWmofm8YBcWFCXas5Lwz9YwrZIw4+FWRX0R4B5dijZJ0jGXP4XX9oNWbMhDNFZAAAAAElFTkSuQmCC&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;单调半格示意&quot; title=&quot;单调半格示意&quot; src=&quot;/static/e9bb110c34e987f0a523ac95b0df0bf3/f058b/e2fa1664f2bd085b4f14c29f6eb79a98af62c5f607ef31b47af562edf6cfb5f8.png&quot; srcSet=&quot;/static/e9bb110c34e987f0a523ac95b0df0bf3/c26ae/e2fa1664f2bd085b4f14c29f6eb79a98af62c5f607ef31b47af562edf6cfb5f8.png 158w,/static/e9bb110c34e987f0a523ac95b0df0bf3/6bdcf/e2fa1664f2bd085b4f14c29f6eb79a98af62c5f607ef31b47af562edf6cfb5f8.png 315w,/static/e9bb110c34e987f0a523ac95b0df0bf3/f058b/e2fa1664f2bd085b4f14c29f6eb79a98af62c5f607ef31b47af562edf6cfb5f8.png 630w,/static/e9bb110c34e987f0a523ac95b0df0bf3/40601/e2fa1664f2bd085b4f14c29f6eb79a98af62c5f607ef31b47af562edf6cfb5f8.png 945w,/static/e9bb110c34e987f0a523ac95b0df0bf3/78612/e2fa1664f2bd085b4f14c29f6eb79a98af62c5f607ef31b47af562edf6cfb5f8.png 1260w,/static/e9bb110c34e987f0a523ac95b0df0bf3/010dc/e2fa1664f2bd085b4f14c29f6eb79a98af62c5f607ef31b47af562edf6cfb5f8.png 1714w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;&lt;br/&gt;
&lt;em&gt;单调半格示意：从左到右单向递增，合并操作被定义为最小上确界(LUB)，所有改动都 deliver 之后状态一致&lt;/em&gt;&lt;/p&gt;&lt;h3 id=&quot;定理&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%AE%9A%E7%90%86&quot; aria-label=&quot;定理 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;定理&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;定理：假设有 Eventual delivery 和 Termination 的属性，那么任意单调半格的 State-based 对象都是有强最终一致性（Strong Eventual Consistency）的。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;直觉上我们应该如何理解这个定理呢？&lt;/p&gt;&lt;p&gt;观察到最小上确界操作 &lt;span class=&quot;math math-inline&quot;&gt;\sqcup&lt;/span&gt; 符合以下性质：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;结合律: &lt;span class=&quot;math math-inline&quot;&gt;(a \sqcup b) \sqcup c&lt;/span&gt; = &lt;span class=&quot;math math-inline&quot;&gt;a \sqcup (b \sqcup c)&lt;/span&gt;&lt;/li&gt;&lt;li&gt;交换律: &lt;span class=&quot;math math-inline&quot;&gt;a \sqcup b&lt;/span&gt; = &lt;span class=&quot;math math-inline&quot;&gt;b \sqcup a&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;从而收到同样的更新列表 u = &lt;span class=&quot;math math-inline&quot;&gt;\{s_0, s_1,...,s_n\}&lt;/span&gt;，但更新顺序不同的副本，他们的最终状态因为有交换律和结合律都等于 &lt;span class=&quot;math math-inline&quot;&gt;s_0 \sqcup s_1 \sqcup s_2 ... \sqcup s_n&lt;/span&gt;，所以此时所有副本的状态都一致，符合 SEC。详细的定理证明见&lt;a href=&quot;https://kirgizov.link/teaching/esirem/information-systems-2021/CRDT/CRDT.pdf&quot;&gt;原文&lt;/a&gt;。&lt;/p&gt;&lt;h3 id=&quot;如何设计-state-based-crdt&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1-state-based-crdt&quot; aria-label=&quot;如何设计 state based crdt permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;如何设计 State-based CRDT&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;保证系统满足 Eventual delivery 和 Termination&lt;/li&gt;&lt;li&gt;state 在 ≤ 上满足偏序集的要求&lt;/li&gt;&lt;li&gt;对任意的 s, u 满足 &lt;code class=&quot;language-text&quot;&gt;s ≤ update(s, u)&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;merge(s, s&amp;#x27;)&lt;/code&gt; 得到的是两个状态的上确界&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;基于操作的-crdtop-based-crdt-cmrdt&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%9F%BA%E4%BA%8E%E6%93%8D%E4%BD%9C%E7%9A%84-crdtop-based-crdt-cmrdt&quot; aria-label=&quot;基于操作的 crdtop based crdt cmrdt permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;基于操作的 CRDT（Op-based CRDT, CmRDT）&lt;/h2&gt;&lt;p&gt;同样的设计一个基于操作的 CRDT 中需要先定义一个 Op-based Object。当它满足一定的性质时就能成为 Op-based CRDT。Op-based Object 的定义包括：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;state&lt;/code&gt; 是每个副本的内部状态&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;state_zero&lt;/code&gt; 内部状态的初始值&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;op&lt;/code&gt; 是每个原子操作的类型，副本直接通过传递 op 来达到同步&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;apply_op(state, op)&lt;/code&gt; 是在一个 state 上应用 op 的函数，返回新的状态；op 的交换律指的就是 &lt;code class=&quot;language-text&quot;&gt;apply_op(apply_op(state, opA), opB) == apply_op(apply_op(state, opB), opA)&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;check_state(state, op)&lt;/code&gt; 确认一个状态是否满足应用 op 的前置条件&lt;ul&gt;&lt;li&gt;Op-based CRDT 的每一个操作 op 都有对应的前置状态检查函数 check_state，在应用 op 之前需要检查 check_state 函数是否满足，如果不满足就将阻塞延迟&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;check_state&lt;/code&gt; 函数的目的是为了保证 op 所依赖的&lt;strong&gt;因果顺序&lt;/strong&gt;成立，例如删除 x 节点的操作依赖于 x 节点被创建的操作，否则就无法应用该操作。这也意味着 Op-based CRDT 的使用者有责任证明每个操作的前置状态都是能够得到满足的&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;副本之间通过传递彼此缺失的 op，并进行 apply_op 来达到最终一致性&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;并行的操作---concurrent-operation&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%B9%B6%E8%A1%8C%E7%9A%84%E6%93%8D%E4%BD%9C---concurrent-operation&quot; aria-label=&quot;并行的操作   concurrent operation permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;并行的操作 - Concurrent Operation&lt;/h3&gt;&lt;p&gt;根据每个 op 在副本上的执行顺序我们可以定义 op 之间的偏序关系：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;如果一个 op A 在某个副本上先于 op B 执行，那么 A &amp;lt; B&lt;/li&gt;&lt;li&gt;如果既没有 A &amp;lt; B 也没有 B &amp;lt; A，那么我们就称 A 和 B 是并行的操作&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;定理-1&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%AE%9A%E7%90%86-1&quot; aria-label=&quot;定理 1 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;定理&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;构建一个 Op-based CRDT 的充分条件为:&lt;/p&gt;&lt;p&gt;假设所有操作都&lt;strong&gt;按照因果顺序 deliver&lt;/strong&gt;，且所有更新函数都会终止（满足 Termination）。那么所有满足以下条件的 op-based object 就具有强最终一致性（SEC）&lt;/p&gt;&lt;ul&gt;&lt;li&gt;所有的并行的操作都满足交换律&lt;/li&gt;&lt;li&gt;每个操作的前置条件都能通过按因果顺序应用得到满足&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;假设所有操作都满足 Eventual Deliver (&lt;strong&gt;按照随机顺序 deliver&lt;/strong&gt;)，且所有更新函数都会终止（满足 Termination）。那么所有满足以下条件的 op-based object 就具有强最终一致性（SEC）&lt;/p&gt;&lt;ul&gt;&lt;li&gt;所有的操作都满足交换律&lt;/li&gt;&lt;/ul&gt;&lt;/blockquote&gt;&lt;p&gt;如何直观理解该定理: 如果有可能冲突的 Op 都是可以交换的，那么它们在一个副本上的应用顺序就对 Object 的最终状态没有影响，那么任意两个应用了同样的 op 集合的 Op-based CRDT 就都具有一致的状态，从而满足 SEC。&lt;/p&gt;&lt;p&gt;详细的定理证明见&lt;a href=&quot;https://kirgizov.link/teaching/esirem/information-systems-2021/CRDT/CRDT.pdf&quot;&gt;原文&lt;/a&gt;。&lt;/p&gt;&lt;h3 id=&quot;如何设计-op-based-crdt&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1-op-based-crdt&quot; aria-label=&quot;如何设计 op based crdt permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;如何设计 Op-based CRDT&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;保证系统满足 Eventual delivery 和 Termination&lt;/li&gt;&lt;li&gt;保证可能出现并行的 Operation 都满足交换律（不管先应用哪个 Op，最终状态都一致）&lt;/li&gt;&lt;li&gt;在应用 Op 时需要保证该 Op 所依赖的前置状态得到满足&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;cmrdt-和-cvrdt-是等价的&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#cmrdt-%E5%92%8C-cvrdt-%E6%98%AF%E7%AD%89%E4%BB%B7%E7%9A%84&quot; aria-label=&quot;cmrdt 和 cvrdt 是等价的 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CmRDT 和 CvRDT 是等价的&lt;/h2&gt;&lt;p&gt;在形式上，Op-based CRDT 和 State-based 是可以互相转换的。思路为：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;通过 Op-based CRDT 构建 State-based CRDT 的方式为: &lt;ul&gt;&lt;li&gt;将新的 State-based Object 的 state 定义为一个二元组(s, M)，s 和 Op-based CRDT 的内部状态一致，M 是 Op-based CRDT 的内部 Op 的集合。&lt;/li&gt;&lt;li&gt;将新的 State-based Object 的 merge 操作定义为 &lt;code class=&quot;language-text&quot;&gt;merge((s, M), (s&amp;#x27;, M&amp;#x27;)) = apply_ops(s, M&amp;#x27; - M)&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;通过 State-based CRDT 构建 Op-based CRDT 的方式为：&lt;ul&gt;&lt;li&gt;将新的 Op-based object 的 Op 定义为 State-based CRDT 的 State &lt;/li&gt;&lt;li&gt;将 &lt;code class=&quot;language-text&quot;&gt;apply_op&lt;/code&gt; 的操作定义为 &lt;code class=&quot;language-text&quot;&gt;apply_op(state, op) = merge(state, op)&lt;/code&gt;，而 merge 是服从对称性的操作，从而我们能够满足 SEC 得到一个 Op-based CRDT&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;设计-crdt&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E8%AE%BE%E8%AE%A1-crdt&quot; aria-label=&quot;设计 crdt permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;设计 CRDT&lt;/h1&gt;&lt;p&gt;根据上文我们有两种设计 CRDT 的方法，汇总为&lt;/p&gt;&lt;h2 id=&quot;如何设计一个-state-based-crdt&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AA-state-based-crdt&quot; aria-label=&quot;如何设计一个 state based crdt permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;em&gt;如何设计一个 State-based CRDT&lt;/em&gt;&lt;/h2&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;需要定义的内容&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;描述&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;State&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;内部状态的类型是什么&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;state_zero: State&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;初始状态&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;≤&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;状态之间的偏序关系&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;merge(s0: State, s1: State): State&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;合并两个状态的方法，应得到 s 和 s’ 的&lt;strong&gt;最小上确界&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Update&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;更新的类型&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;update(s: State, u: Update): State&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;定义状态的更新方式，&lt;span class=&quot;math math-inline&quot;&gt;\forall s, u&lt;/span&gt;. s ≤ update(s, u)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;同时我们要保证&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Eventual Delivery&lt;/li&gt;&lt;li&gt;每个函数都会终止&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;如何设计一个-op-based-crdt&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AA-op-based-crdt&quot; aria-label=&quot;如何设计一个 op based crdt permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;em&gt;如何设计一个 Op-based CRDT&lt;/em&gt;&lt;/h2&gt;&lt;p&gt;假设 op 不一定按照因果顺序 deliver，那么设计的方式为&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;需要定义的内容&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;描述&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;State&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;内部状态的类型是什么&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;state_zero: State&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;初始状态&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Op&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;操作的类型&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;apply(state: State, op: Op): State&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;应用一个操作的函数，需要满足交换律，即 &lt;span class=&quot;math math-inline&quot;&gt;\forall o_a, o_b, s&lt;/span&gt;. apply(apply(s, &lt;span class=&quot;math math-inline&quot;&gt;o_a&lt;/span&gt;), &lt;span class=&quot;math math-inline&quot;&gt;o_b&lt;/span&gt;) = apply(apply(s, &lt;span class=&quot;math math-inline&quot;&gt;o_b&lt;/span&gt;), &lt;span class=&quot;math math-inline&quot;&gt;o_a&lt;/span&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;check_state(s: State, op: Op): boolean&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;确认一个 op 所依赖的前置条件是否被满足&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;同时我们要保证&lt;/p&gt;&lt;ul&gt;&lt;li&gt;所有的 Op 最终都会被发布到每一个副本上&lt;/li&gt;&lt;li&gt;每个函数都会终止&lt;/li&gt;&lt;li&gt;保证每个 op 的前置条件最终都能得到满足&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;设计一个可增加删除元素的-set-crdt&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AA%E5%8F%AF%E5%A2%9E%E5%8A%A0%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0%E7%9A%84-set-crdt&quot; aria-label=&quot;设计一个可增加删除元素的 set crdt permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;设计一个可增加删除元素的 Set CRDT&lt;/h2&gt;&lt;p&gt;基于上面的 CRDT 框架我们已经有很好的理论支持我们设计一个 Last-write-wins Set，即出现同时删除和添加同一个元素时后写入的操作将覆盖先写入的操作。&lt;/p&gt;&lt;p&gt;但是在设计之前需要先了解在分布式系统中我们该如何判断事件的“先后”。&lt;/p&gt;&lt;h3 id=&quot;lamport-timestamp&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#lamport-timestamp&quot; aria-label=&quot;lamport timestamp permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Lamport Timestamp&lt;/h3&gt;&lt;p&gt;在中心化的系统中我们可以通过事件到达中心服务器的时间作为时间发生的时间戳来判断先后。但是在分布式环境中这种方式就不能使用了。此时我们可以使用 &lt;a href=&quot;https://en.wikipedia.org/wiki/Lamport_timestamp&quot;&gt;Lamport Timestamp&lt;/a&gt;。&lt;/p&gt;&lt;p&gt;Lamport Timestamp 的算法很简单&lt;/p&gt;&lt;ul&gt;&lt;li&gt;每个进程维护一个 counter&lt;/li&gt;&lt;li&gt;本地每发生一个事件就将 counter + 1，并将事件的时间戳设置为 counter 值&lt;/li&gt;&lt;li&gt;每当进程发送一个消息，就将本地 counter + 1，并将最新的 counter 值附带在消息上&lt;/li&gt;&lt;li&gt;当进程收到消息后，让自己的 &lt;code class=&quot;language-text&quot;&gt;counter = max(counter, message.counter) + 1&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;从而每一个消息都有一个明确的时间戳，根据时间戳我们就能够得到消息间的全序关系。但为了能够处理消息的 counter 相同的情况，我们还需要在消息中带上进程自己的 id，从而能够将全序关系定义为: &lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;a.counter == b.counter? 
  a.pid &amp;lt; b.pid : a.counter &amp;lt; b.counter&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:81.64556962025317%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAABRNAAAUTQGUyo0vAAABvElEQVQ4y5VU21KDMBTk/z9Nn+yLo22xYlvu9xDuXWdPhaYUnTEzkJDkbM7unmABQBTkaOqOQ1wul7mfxmvza/vYW3w5to8iq6DKWhbatp+BdNWg74b5O08r9P3tm3HDMN4AufjytMdh68E7JahUA/+cIolKCQ69TMZZopDGCnGQy5hAaVwicLO7TCXD140jwSbtcbzSIJg5P9FjVgQjqCmJZWq0plkSFihz/aAfnyxWqHV7FyOAoZ+jbfpV0QlmamY20p72jeNVR6H8ufORRuVdJmwEYhY0i+ZQu6EfROey0DgeQsmQsWQigAzaPNv42HqywM15qkRTAsRhgdNnJHoFboqqrGUPD1GFhq5aqKJG23Q3De13V0AmI5aU6TR7rRr81WYNl5MmILNkBqRkOrr2zBl6x2QW3nSajRSp06Tvcs/DTenaHs7el9Pp2uQeQXglz04kmtlvrtQe15gA12kUdTcztygmTTnsPHGUAaTHgOznZky3gtQ5lqyrVgzhYVMtCuA4jGJ/kekHuv9pDzdlqQfvNUuFDjM7Fj/Lh4dzjRWx9uex1pxiTyoEOn/Fc91F/vXHQA2HX0z8Btos4K0UdNj9AAAAAElFTkSuQmCC&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Lamport timestamp&quot; title=&quot;Lamport timestamp&quot; src=&quot;/static/d388264809b888a24cad6a017a7ab651/f058b/98d737fca39479040c2c821e63b4fa66e614c4ee51fc27a4abb21eadd4a0f1de.png&quot; srcSet=&quot;/static/d388264809b888a24cad6a017a7ab651/c26ae/98d737fca39479040c2c821e63b4fa66e614c4ee51fc27a4abb21eadd4a0f1de.png 158w,/static/d388264809b888a24cad6a017a7ab651/6bdcf/98d737fca39479040c2c821e63b4fa66e614c4ee51fc27a4abb21eadd4a0f1de.png 315w,/static/d388264809b888a24cad6a017a7ab651/f058b/98d737fca39479040c2c821e63b4fa66e614c4ee51fc27a4abb21eadd4a0f1de.png 630w,/static/d388264809b888a24cad6a017a7ab651/40601/98d737fca39479040c2c821e63b4fa66e614c4ee51fc27a4abb21eadd4a0f1de.png 945w,/static/d388264809b888a24cad6a017a7ab651/78612/98d737fca39479040c2c821e63b4fa66e614c4ee51fc27a4abb21eadd4a0f1de.png 1260w,/static/d388264809b888a24cad6a017a7ab651/76a18/98d737fca39479040c2c821e63b4fa66e614c4ee51fc27a4abb21eadd4a0f1de.png 1691w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;&lt;br/&gt;
&lt;em&gt;Lamport timestamp，每个进程都会维护自己的计数器&lt;/em&gt;&lt;/p&gt;&lt;h3 id=&quot;实现&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%AE%9E%E7%8E%B0&quot; aria-label=&quot;实现 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;实现&lt;/h3&gt;&lt;p&gt;以 Op-based CRDT 的思路设计 Last-write-wins Set(LWWSet):&lt;/p&gt;&lt;ul&gt;&lt;li&gt;通过 Lamport timestamp 定义 Op 之间的全序关系&lt;/li&gt;&lt;li&gt;存在共同修改同一个元素的操作时，让 timestamp 更大的 Op 获胜&lt;ul&gt;&lt;li&gt;如果 Timestamp 一致，则让 pid 更大的 Op 获胜&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;details&gt;&lt;summary&gt;TypeScript 实现代码&lt;/summary&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Op&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  time&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  pid&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;add&amp;quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;remove&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;State&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Map&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Op&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ProcessState&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; counter&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; state&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; State &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; state_zero&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; State &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; process_state_zero&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ProcessState &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;counter&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; state&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; state_zero&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;state&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;ProcessState&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; op&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Op&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ProcessState &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; new_state&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ProcessState &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    counter&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;counter&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; op&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;time&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    state&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;new_state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;op&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    new_state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;op&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; op&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; old_op &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;op&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      old_op&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;time &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; op&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;time &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;old_op&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;time &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; op&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;time &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; old_op&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pid &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; op&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pid&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      new_state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;op&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; op&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; new_state&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;check_state&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;state&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; State&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; op&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Op&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 判断 LWWSet 是否含有某个值&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;state&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; State&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; v&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;v&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;add&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 插入 v 到 LWWSet&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;state&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ProcessState&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; v&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ProcessState &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    time&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;counter&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    pid&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getPid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;add&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; v&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 删除 LWWSet 中的 v&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;state&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ProcessState&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; v&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ProcessState &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    time&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;counter&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    pid&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getPid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;remove&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; v&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/details&gt;</content:encoded></item><item><title><![CDATA[Introduction to CRDTs]]></title><description><![CDATA[中文版本    CRDT (conflict-free replicated data type) is a data structure that can be replicated across multiple computers in a network, where…]]></description><link>https://www.zxch3n.com/crdt-intro/crdt-intro.en/</link><guid isPermaLink="false">https://www.zxch3n.com/crdt-intro/crdt-intro.en/</guid><pubDate>Tue, 21 Dec 2021 22:00:02 GMT</pubDate><content:encoded>&lt;blockquote&gt;&lt;p&gt;&lt;a href=&quot;/crdt-intro/crdt-intro&quot;&gt;中文版本&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:62.65822784810127%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABRNAAAUTQGUyo0vAAACDUlEQVQ4y21Ta08aURDlP/cH9Eu/90P7sWn6iIk12hhrH1apxUjVlCJYEAEFljfL8thddln2wWnObFkUmWRz796Ze+bMnLmx+XyOhQVBIGu/O4bjuLhvjFv3BcFyT4uFQHPYlhOBDjUDnuvDnjhQ7npwpu4j4MV+NakAGmMbrbqGiTlFpdRBLl1Drz0SMLKlb2rP4HvB8nIQgl1nFexuJKF2RiFD07BRKXYEbDycQFN1cbQUDd3WEK7rodscQrntydqo9jExphLD+K3XCRx/yeBw708IyKylXBOJgyvJmkoWcZ4oCP3UaRGfNs/QrPWRSpbw7uUh7m7aUakEvLmqC2D+shYCspSP709hjC3p4/OnOzjYTUkwA0v5JjZfHcv/t53fUckskWf9ni4lL9oQUztjvH3xXXpmmVM8e7KBXz/ysK0Ztt+cYP/DuWSnSJ+3LiJ21sTB31QF8f1LFLJKKKgfhAwzF7f4+TULU7cxGphgEvaukK2jVu6KKGTUbgzgzrwH6h7tpcUfqcxAZqO6jaoKfWRFwTxnWUzKdlTLXdQrqvh8P1Q8Gc/JJESAVI9N5wWWRbV56TqjiABkS8W5siWrjyAZz2OkmUtAZmKZncZAGPIigWYzD77ny1C7rv9osIP/c5g+K0Pr6Q8Hm0Z2pE6GA9XAqq17djTOLSckArzvXDxDirMOZPW5rdv/A5Ig2MSHd6a7AAAAAElFTkSuQmCC&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03&quot; title=&quot;355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03&quot; src=&quot;/static/a1d092aa8bcaf737b8de0e1279057dab/f058b/355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03.png&quot; srcSet=&quot;/static/a1d092aa8bcaf737b8de0e1279057dab/c26ae/355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03.png 158w,/static/a1d092aa8bcaf737b8de0e1279057dab/6bdcf/355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03.png 315w,/static/a1d092aa8bcaf737b8de0e1279057dab/f058b/355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03.png 630w,/static/a1d092aa8bcaf737b8de0e1279057dab/40601/355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03.png 945w,/static/a1d092aa8bcaf737b8de0e1279057dab/78612/355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03.png 1260w,/static/a1d092aa8bcaf737b8de0e1279057dab/e8950/355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03.png 2000w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;  &lt;/p&gt;&lt;p&gt;CRDT (conflict-free replicated data type) is a data structure that can be replicated across multiple computers in a network, where replicas can be updated independently and in parallel, without the need for coordination between replicas, and with a guarantee that no conflicts will occur.&lt;/p&gt;&lt;p&gt;CRDT is often used in collaborative software, such as scenarios where multiple users need to work together to edit/read a shared document, database, or state. It can be used in database software, text editing software, chat software, etc.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#what-problems-does-crdt-solve&quot;&gt;What problems does CRDT solve?&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#origins&quot;&gt;Origins&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#a-simple-crdt-case&quot;&gt;A simple CRDT case&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#introduction-to-the-principle&quot;&gt;Introduction to the Principle&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#comparison-of-crdt-and-ot&quot;&gt;Comparison of CRDT and OT&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#addressed-and-unresolved-issues&quot;&gt;Addressed and Unresolved Issues&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#start-using-crdts-today&quot;&gt;Start Using CRDTs Today&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#extended-reading&quot;&gt;Extended reading&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;what-problems-does-crdt-solve&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#what-problems-does-crdt-solve&quot; aria-label=&quot;what problems does crdt solve permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What problems does CRDT solve?&lt;/h1&gt;&lt;p&gt;For example, a scenario where multiple users edit the same document online at the same time&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:100%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABRNAAAUTQGUyo0vAAACw0lEQVQ4y4VUiU7iUBTt/3/EJBpxFARGjJo4jtGMDO4Le1sUWjYplKVQuvdM7oNiWZxp8tKb917Ou/fccy7n+z5oBV8Qh/fCX/g8iD3PW5xzQdDrjaHr5j/BNj3ouh4qlQ8Mhzrb44LDRqOPUqkJx3GXMvgq+yA2DBut1gDdrgbLcmaAlHK9riKbldcAN8XB33E8VpUkddHpjD4BCaRcbmEyMdkmlbGa5SpnBJBOCygWm9A0A7VaF7Y9ByQQWe6xy6o6ZpxMpxY8L8ylP1+ALPXY46L4gUZDZfff3xWWGDfjwWL8ER8BL5dXZUiyCtd1YVsO7BkTKPNtvL0rLCbunp9ryOXqqFQ6sO05IAWULgEFZTX4KornV8gdnSCbTIG/vsPbQxa5058Q7l7Znc7HEO32gMX9/mTG4apEAk0phTykw30IR4fIJxJ42okg820br7E4+ItLmIa11v2FDpdIDzJ8ySIf2cJjZBcP0QTuv8dwv70D+TQJ8ewMxnSuWW9ZCdwmZ7Q7GmrpP6gcRPC4tYWH3X3c7saQ2YmCj++hkIhDH+sb9cmFN6g51aoCQexAzmTQPIlBSMbAp36glEqhdHyC1sUp+KNDvNwUkS8019zFfWrLh1TrwvV8uJbFeGqcH6MQiyJ/cIDs/h5y0Sj4RBRP8RS0gYbhcIpms79e8nhsQFE0KMoI44kJQzdQFeqoFUSI6TsI17fgf2dQ/nWJSvoGxWcRJtORzzobdhdHmQU+JINLUo8JduNgoGEwp3o00hcepkU2ZCUTIGmIDjVtitFoygDJfvQiyYg6uWo/qopcRVXRpFrIhi7QC6o6wWCgsyzJhmS9TX4OjEDNaLeHrCK6S2thPXIJkTtL3WWARMGqnMINNE2H8U6gNByoMtO0PwcslUkThD7KNDxxNg1YOicAGhLB3UXJ/xv3Xw3YGcfLlPwFuqz10OMmQh4AAAAASUVORK5CYII=&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Multi-user editing&quot; title=&quot;Multi-user editing&quot; src=&quot;/static/dd72b43c308694b1853565c1ab6ef774/f058b/22a11d131ecdc2622d427594857658ffea14634328c973a06777089de599970e.png&quot; srcSet=&quot;/static/dd72b43c308694b1853565c1ab6ef774/c26ae/22a11d131ecdc2622d427594857658ffea14634328c973a06777089de599970e.png 158w,/static/dd72b43c308694b1853565c1ab6ef774/6bdcf/22a11d131ecdc2622d427594857658ffea14634328c973a06777089de599970e.png 315w,/static/dd72b43c308694b1853565c1ab6ef774/f058b/22a11d131ecdc2622d427594857658ffea14634328c973a06777089de599970e.png 630w,/static/dd72b43c308694b1853565c1ab6ef774/40601/22a11d131ecdc2622d427594857658ffea14634328c973a06777089de599970e.png 945w,/static/dd72b43c308694b1853565c1ab6ef774/78612/22a11d131ecdc2622d427594857658ffea14634328c973a06777089de599970e.png 1260w,/static/dd72b43c308694b1853565c1ab6ef774/a878e/22a11d131ecdc2622d427594857658ffea14634328c973a06777089de599970e.png 2048w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;  &lt;/p&gt;&lt;p&gt;This scenario requires that each user sees the same content, even after concurrent edits by different users (e.g. two users changing the title at the same time), which is known as &lt;strong&gt;consistency&lt;/strong&gt;. (To be precise, CRDT satisfies the eventual consistency, see below for more details)&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:100%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABRNAAAUTQGUyo0vAAACo0lEQVQ4y41Uy27bMBD0f/bWAr21p/5cbwUa9JpDgCSuHT/i6C2KEkW9JU8xK0txHAcoAUIkRc7uDHe4OB6PYJ/aND9fu2yXe4ZhmP8tpkEc5yiK+s3Bru3RdT2qspEu87Z/s6fvB6zXPtK0kACLKeLLS4KHB0cA6qqBVhZpYpEqi9yUsKYa56e1TBcCWFUtXFcjigyaphsBmfLhoHB3d0BuKjk8DCOdLC2wW/mIg2zOqixqqNBIdmS130cIguwVkFk9ProzZR4m6ESXGVZFI3OTFrB5BWtr3NwscX/vwJgKu12Etj0BEvn5OZ4FH/pBsiA9AuRZKZ00GWBkdcRy6YlU1HCzCSXgYtShEf3qusX/tONJDs9LcXu7E6nW6wBtewLkgOlSYDZSOjs+Zz61qUx8P5ULYUsSO2p4rd6KsoHaRqhUjq4HTKpkve16WD+FCQ2OVzKe6/C8UKcAbVUjOBQInJ9Y331Bqn4hNx2cTQCrC7ko6jrJNJ1dXFb/OB4pNXWEXH2Fs/kEHXxD6B2wXUU4bEMErobvJLM8M+BldiPoAEJG3h/U+Xd4u8+w+gdUsPzQiu+sd9mSbQx3H8CkewTOb6TqLzxXIX4K0VftO6A3GRpTihezbPw2dYciLa4GsqFB4mrRjk5h7bFPoAuWAH3IDVpbKfDVyhfK/Nc2Lfquky8DTU0nFp6nEYZGzhNcKLPiWUOsJ1qIWa7WvkQt8gpa5QicRL58MOJw9DStp1SOMMzkpZo1ZJqMoJSF1oVQpp1CPxWb0XpPj+6sF4PQCPQ9nbLfxyjLRvpsPbrEcRIB5ma+PDyQaSuZ0cMc09tN3Qo4JSJdgk7MuDbfMnULTk8UM52eLxH+9LCe3yrXCUDqk34z5avP/Qfrsv+sTBj4vIb/AcqoDtCaMeu9AAAAAElFTkSuQmCC&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;CRDT in P2P connection&quot; title=&quot;CRDT in P2P connection&quot; src=&quot;/static/588890f957763c2d836bf9a0bf899066/f058b/7a948606a9246f0f632c382370b79519e046e92c8745b000c35675c215456baa.png&quot; srcSet=&quot;/static/588890f957763c2d836bf9a0bf899066/c26ae/7a948606a9246f0f632c382370b79519e046e92c8745b000c35675c215456baa.png 158w,/static/588890f957763c2d836bf9a0bf899066/6bdcf/7a948606a9246f0f632c382370b79519e046e92c8745b000c35675c215456baa.png 315w,/static/588890f957763c2d836bf9a0bf899066/f058b/7a948606a9246f0f632c382370b79519e046e92c8745b000c35675c215456baa.png 630w,/static/588890f957763c2d836bf9a0bf899066/40601/7a948606a9246f0f632c382370b79519e046e92c8745b000c35675c215456baa.png 945w,/static/588890f957763c2d836bf9a0bf899066/78612/7a948606a9246f0f632c382370b79519e046e92c8745b000c35675c215456baa.png 1260w,/static/588890f957763c2d836bf9a0bf899066/a878e/7a948606a9246f0f632c382370b79519e046e92c8745b000c35675c215456baa.png 2048w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;  &lt;/p&gt;&lt;p&gt;Users can use CRDT even when they are offline. They can be back on sync with others the network is restored. It also supports collaboratively editing with other users via P2P. It is known as &lt;strong&gt;partitioning fault tolerance&lt;/strong&gt;. This allows CRDT to support &lt;strong&gt;decentralized&lt;/strong&gt; applications very well: synchronization can be done even without a centralized server.&lt;/p&gt;&lt;h1 id=&quot;origins&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#origins&quot; aria-label=&quot;origins permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Origins&lt;/h1&gt;&lt;p&gt;The formal definition of CRDT first appears in Marc Shapiro’s 2011 paper &lt;a href=&quot;https://readpaper.com/paper/1516319412&quot;&gt;Conflict-free replicated data types&lt;/a&gt; (but &lt;a href=&quot;https://doi.org/10.1145%2F1180875.1180916&quot;&gt;Woot&lt;/a&gt; in 2006 is probably the earliest study). The motivation for the proposal is that the conflict resolution design of &lt;em&gt;Eventual Consistency&lt;/em&gt; is difficult, few articles give design guidance suggestions, and randomly designed solutions are error-prone. So this paper proposes a simple, theoretically proven way to achieve Eventual Consistency, i.e., CRDT.&lt;/p&gt;&lt;p&gt;(PS: Marc Shapiro actually wrote a paper &lt;a href=&quot;https://hal.inria.fr/inria-00177693v2/document&quot;&gt;Designing a commutative replicated data type&lt;/a&gt; in 2007. In 2011, he reworded commutative into conflict-free in 2011, expanding the definition of commutative to include state-based CRDT)&lt;/p&gt;&lt;p&gt;According to &lt;a href=&quot;https://en.wikipedia.org/wiki/CAP_theorem&quot;&gt;CAP theorem&lt;/a&gt;, it is impossible for a distributed computing system to perfectly satisfy the following three points at the same time.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;em&gt;Consistency&lt;/em&gt;: each read receives the result of the most recent write or reports an error; it behaves as if it is accessing the same piece of data&lt;/li&gt;&lt;li&gt;&lt;em&gt;Availability&lt;/em&gt;: every request gets a non-error response - but there is no guarantee that the data fetched is up-to-date&lt;/li&gt;&lt;li&gt;&lt;em&gt;Partition tolerance&lt;/em&gt;: the ability of a distributed system to continue functioning properly even when communication between its different components is lost or delayed, resulting in a partition or network failure.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If the system cannot achieve data consistency within the time limit, it means that partitioning has occurred and a choice must be made between C and A for the current operation, so “perfect consistency” is in conflict with “perfect availability”.&lt;/p&gt;&lt;p&gt;CRDTs do not provide “perfect consistency”, but &lt;a href=&quot;#-strong-eventual-consistency-sec&quot;&gt;Strong Eventual Consistency (SEC)&lt;/a&gt;. This means that site A may not immediately reflect the state changes from site B, but when A and B synchronize their messages they both regain consistency and do not need to resolve potential conflicts (CRDT mathematically prevents conflicts from occurring). &lt;em&gt;Strong Eventual Consistency&lt;/em&gt; does not conflict with &lt;em&gt;Availability&lt;/em&gt; and &lt;em&gt;Partition Tolerance&lt;/em&gt;. CRDTs provide a good CAP tradeoff.&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:52.53164556962025%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABRNAAAUTQGUyo0vAAABx0lEQVQoz42TXUvcQBSG90f6EwqCUCpI7UV7o2DZC3sl1CsLFZFVC26tslCqKLV1KUpZupbidmXNfpk0ySaTNMkmM/OWmXw47XrRA8Nkzkmec945JyXOOcQSFo8TjKMEURiDUiZ9eSw7TPi46uMcJRVoGQSdlg6tbSDwo8kPlD2OEiQxlRA1VlJfYozh54/Bvdm5ck4ow3VbR16niOaxv4A+CTHQTLgj/w6kVsDS3WvpaB00oFUv4DW0DJpVWGSNKYzhSD4T5zdGlpdVncPSO/Uu+zh7XEHzRQ31Z2/wcW4TTr1dJCyAxA0kxP5F4Ng+bvu2bI6EZg1iToBPjzawPf0KO0938HLmNbbnK2gu10B1cifZGDpSrgCI6jw3QBiMJdTU3eJOv5X3cfRgDcfldzhaeovD53v4vFjFycN1NJZr4FGSAlvfexh0LQkzDVdKFyABzKVTEuLD1ApOZjdwuXqIxsp7nC/s4my+gvqTLZwvVkFNHyVLJ+h3TAw1Kx0DxWzDg94dgSapnzR7sL9cw7nowDy9gvP1BlHPRjx0wd0wlSyGWcgTSwyz2lkBNHoOaMLwP1bM4b/OiT8kHyHGZCeLpExZme8P3gpBDHChkBgAAAAASUVORK5CYII=&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;CPA&quot; title=&quot;CPA&quot; src=&quot;/static/ccafe0751b5557ae551b634eb9c47e16/f058b/a4858e2a50bc1a2d79722060156e89b0cac5815cf25e8c67e409aa0926280cef.png&quot; srcSet=&quot;/static/ccafe0751b5557ae551b634eb9c47e16/c26ae/a4858e2a50bc1a2d79722060156e89b0cac5815cf25e8c67e409aa0926280cef.png 158w,/static/ccafe0751b5557ae551b634eb9c47e16/6bdcf/a4858e2a50bc1a2d79722060156e89b0cac5815cf25e8c67e409aa0926280cef.png 315w,/static/ccafe0751b5557ae551b634eb9c47e16/f058b/a4858e2a50bc1a2d79722060156e89b0cac5815cf25e8c67e409aa0926280cef.png 630w,/static/ccafe0751b5557ae551b634eb9c47e16/40601/a4858e2a50bc1a2d79722060156e89b0cac5815cf25e8c67e409aa0926280cef.png 945w,/static/ccafe0751b5557ae551b634eb9c47e16/78612/a4858e2a50bc1a2d79722060156e89b0cac5815cf25e8c67e409aa0926280cef.png 1260w,/static/ccafe0751b5557ae551b634eb9c47e16/9ac09/a4858e2a50bc1a2d79722060156e89b0cac5815cf25e8c67e409aa0926280cef.png 2204w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;
&lt;em&gt;CRDT satisfies A + P + Eventual Consistency; a good tradeoff under CAP&lt;/em&gt;&lt;/p&gt;&lt;p&gt;(PS: In 2012, Eric Brewer, author of the CAP theorem, wrote an article &lt;a href=&quot;https://www.infoq.com/articles/cap-twelve-years-later-how-the-rules-have-changed/&quot;&gt;CAP Twelve Years Later: How the “Rules” Have Changed&lt;/a&gt;, explaining that the description of the “two out of three CAP features” is actually misleading, and that the CAP actually prohibits perfect availability and consistency in a very small part of the design space, i.e., in the presence of partitions; in fact, the design of the tradeoff between C and A is very flexible. A good example is CRDT.)&lt;/p&gt;&lt;h1 id=&quot;a-simple-crdt-case&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#a-simple-crdt-case&quot; aria-label=&quot;a simple crdt case permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;A simple CRDT case&lt;/h1&gt;&lt;p&gt;We can use a few simple examples to get a general idea of how CRDTs achieve &lt;strong&gt;Strong Eventual Consistency&lt;/strong&gt;.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Grow-only Counter&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;How can we count the number of times something happens in a distributed system without locking?&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;/a18b381322866b3752e3f376f6ccd31d/G-Counter.gif&quot;/&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Let each copy increments only its own counter =&amp;gt; no locking synchronization &amp;amp; no conflicts&lt;/li&gt;&lt;li&gt;Each copy keeps the count values of all other copies at the same time&lt;/li&gt;&lt;li&gt;Number of occurrences = sum of count values of all copies&lt;/li&gt;&lt;li&gt;Since each copy only updates its own count and does not conflict with other counters, this type satisfies consistency after message synchronization&lt;/li&gt;&lt;/ul&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Grow-only Set&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;img src=&quot;/18190698321a05380c6e984d93b77032/G-Set.gif&quot;/&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The elements in a Grow-only Set can only be increased and not decreased&lt;/li&gt;&lt;li&gt;To merge two such states, you only need to do a merge set&lt;/li&gt;&lt;li&gt;This type satisfies consistency after message synchronization because there are no conflicting operations since the elements only grow and do not decrease.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Both of these methods are CRDTs, and they both satisfy the following properties&lt;/p&gt;&lt;ul&gt;&lt;li&gt;They can both be updated independently and concurrently, without coordination (locking) between replicas&lt;/li&gt;&lt;li&gt;There is no possibility of conflict between multiple updates&lt;/li&gt;&lt;li&gt;Final consistency can always be guaranteed&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;introduction-to-the-principle&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#introduction-to-the-principle&quot; aria-label=&quot;introduction to the principle permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Introduction to the Principle&lt;/h1&gt;&lt;p&gt;There are two types of CRDTs: Op-based CRDTs and State-based CRDTs. This article focuses on the concept of Op-based CRDTs.&lt;/p&gt;&lt;p&gt;Op-based CRDTs operate on the principle that if two users perform identical sequences of operations, the final state of the document should also be identical. To achieve this, each user saves all the operations performed on the data (Operations) and synchronizes these Operations with other users to ensure a consistent final state. A critical challenge in this approach is ensuring the order of Operations remains consistent, especially when parallel modification operations occur. To address this, Op-based CRDTs require that all possible parallel Operations be commutative, satisfying the final consistency requirement.&lt;/p&gt;&lt;p&gt;If you want to see how State-based CRDT works, and other more in-depth content welcome to read the next chapter of this series &lt;a href=&quot;./design-crdt&quot;&gt;How to design CRDT&lt;/a&gt;.&lt;/p&gt;&lt;h1 id=&quot;comparison-of-crdt-and-ot&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#comparison-of-crdt-and-ot&quot; aria-label=&quot;comparison of crdt and ot permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Comparison of CRDT and OT&lt;/h1&gt;&lt;p&gt;Both CRDT and &lt;a href=&quot;https://en.wikipedia.org/wiki/Operational_transformation&quot;&gt;Operation Transformation(OT)&lt;/a&gt; can be used in online collaborative applications, with the following differences&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;OT&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;CRDT&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;OT relies on a centralized server for collaboration; &lt;a href=&quot;https://digitalfreepen.com/2018/01/04/operational-transform-hard.html&quot;&gt;it is extremely difficult to make it work in a distributed environment&lt;/a&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;CRDT algorithm can be used to synchronize data through a P2P approach synchronization&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;The earliest paper on OT was presented in 1989&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;The earliest paper on CRDT appeared in 2006&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;The OT algorithm is designed with higher complexity to ensure consistency&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;The CRDT algorithm is designed to be simpler to ensure consistency&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;It is easier to design OT to preserve user intent&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;It is more difficult to design a CRDT algorithm that preserves user intent&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;OT does not affect document size&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;CRDT documents are larger than the original document data&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;More related discussions can be found in&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.infoq.com/presentations/crdt-distributed-consistency/&quot;&gt;CRDTs and the Quest for Distributed Consistency&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://josephg.com/blog/crdts-are-the-future/&quot;&gt;I was wrong. CRDTs are the future&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;addressed-and-unresolved-issues&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#addressed-and-unresolved-issues&quot; aria-label=&quot;addressed and unresolved issues permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Addressed and Unresolved Issues&lt;/h1&gt;&lt;blockquote&gt;&lt;p&gt;This section was last updated in December 2021&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Why do we still see OT algorithms rather than CRDTs in most collaborative software? First, because CRDT is a relatively young method compared to OT, and some of the difficulties have only been solved in recent years, e.g.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;How to make the merge result of conflicting edits meet the user’s intents as much as possible (this problem is often easier in OT than in CRDT)&lt;ul&gt;&lt;li&gt;Related paper &lt;a href=&quot;https://martin.kleppmann.com/papers/interleaving-papoc19.pdf&quot;&gt;Interleaving anomalies in collaborative text editors&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;CRDT Move&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://martin.kleppmann.com/papers/move-op.pdf&quot;&gt;A highly-available move operation for replicated trees&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://martin.kleppmann.com/papers/list-move-papoc20.pdf&quot;&gt;Moving Elements in List CRDTs&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;How to achieve high-performance CRDTs&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There is still a lot of research to be done in the following areas&lt;/p&gt;&lt;ul&gt;&lt;li&gt;CRDTs often have tombstone data that is difficult to recycle, how can we do it better?&lt;/li&gt;&lt;li&gt;How to reduce the overhead of updating CRDT documents?&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;start-using-crdts-today&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#start-using-crdts-today&quot; aria-label=&quot;start using crdts today permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Start Using CRDTs Today&lt;/h1&gt;&lt;p&gt;You don’t need to design and implement CRDT algorithms from scratch (CRDTs can be easily implemented poorly). Instead, you can build your application directly on top of open-source CRDT projects such as &lt;a href=&quot;https://github.com/automerge/automerge&quot;&gt;Automerge&lt;/a&gt;, &lt;a href=&quot;https://github.com/yjs/yjs&quot;&gt;Yjs&lt;/a&gt;, and the one I am currently developing, &lt;a href=&quot;https://loro.dev&quot;&gt;Loro&lt;/a&gt;. You can view their performance comparison &lt;a href=&quot;https://www.loro.dev/docs/performance&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;h1 id=&quot;extended-reading&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#extended-reading&quot; aria-label=&quot;extended reading permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Extended reading&lt;/h1&gt;&lt;ul&gt;&lt;li&gt;Marc Shapiro, Nuno Preguiça, Carlos Baquero and Marek Zawirski, &lt;a href=&quot;https://readpaper.com/paper/1516319412&quot;&gt;Conflict-free replicated data types&lt;/a&gt; (2011 paper formally defining CRDTs)&lt;/li&gt;&lt;li&gt;Petru Nicolaescu, Kevin Jahns, Michael Derntl and Ralf Klamma, &lt;a href=&quot;https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types&quot;&gt;Near Real-Time Peer-to-Peer Shared Editing on Extensible Data Types&lt;/a&gt; (Yjs paper)&lt;/li&gt;&lt;li&gt;Martin Kleppmann, Alastair R. Beresford, &lt;a href=&quot;https://arxiv.org/abs/1608.03960&quot;&gt;A Conflict-Free Replicated JSON Datatype&lt;/a&gt; (Automerge)&lt;/li&gt;&lt;li&gt;Martin Kleppmann, &lt;a href=&quot;https://martin.kleppmann.com/2020/07/06/crdt-hard-parts-hydra.html&quot;&gt;CRDTs The Hard Parts&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Seph, &lt;a href=&quot;https://josephg.com/blog/crdts-go-brrr/&quot;&gt;5000x faster CRDTs: An Adventure in Optimization&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Go to the next chapter: &lt;a href=&quot;../design-crdt&quot;&gt;How to Design a CRDT Algorithm&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;../design-crdt&quot;&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:58.86075949367089%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABRNAAAUTQGUyo0vAAAByUlEQVQoz41Ta1PaQBTl//+JfnE6HduxVqfqOLZWRRGwYHgFaEhIIAbyBJJATLI5nb2WoIzTYWc2u7mz95x77p4tAECWZTS394xlYIzl8V1GgX+WQYS5F1LANue0uvYCoqBiKE2wWj6/IfvfJMDQX8GazBAGETTZRJKkkPsGHssS6vcSFrMwr4ClLE9+s2eMFL0ABhFc26eE8dCiA8swQrehQRQ0iqdJijRlu0mO44SATMOD2NAIMFrFaNUUCFWZ5I9VC4d7tyhddnB1JqBy08XdzzYujmooXrRQK/2BOpi+AHL2XmuEdn2I5oMMf74kUNOYQRJ1PEcxya6XJVSLPTQeZNz+aOPzh2s0fyvot8f4dSoQMAE61gIzN8D1eRNCZUDV8H7oqg2lb7wrjatK4jT/533PL8VzfKru+FMJ3z7eQZWmCPwVSdI1O0/iBLy6kWLi5EsFX/eKlHN2UIXcMzY95AenTx6+75dJ1vAfIL9lLn89jLGDjqDCcwJ0myPqWedRxZPmUM9zQM441V1MdBcjxUL5SoRjzsmTXPquxt74MIioZG5koTog5m3DrhM4wWvf8TVjmzOF9YZ7jL+I143eZt9+nu+R/gVzCpDXGJkojAAAAABJRU5ErkJggg==&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;design CRDT&quot; title=&quot;design CRDT&quot; src=&quot;/static/4e78d08dad64b835622765444af0c517/f058b/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png&quot; srcSet=&quot;/static/4e78d08dad64b835622765444af0c517/c26ae/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 158w,/static/4e78d08dad64b835622765444af0c517/6bdcf/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 315w,/static/4e78d08dad64b835622765444af0c517/f058b/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 630w,/static/4e78d08dad64b835622765444af0c517/40601/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 945w,/static/4e78d08dad64b835622765444af0c517/78612/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 1260w,/static/4e78d08dad64b835622765444af0c517/4e814/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 3211w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[CRDT 简介]]></title><description><![CDATA[English version    CRDT (conflict-free replicated data type) 无冲突复制数据类型，是一种可以在网络中的多台计算机上复制的数据结构，副本可以独立和并行地更新，不需要在副本之间进行协调，并保证不会有冲突发生。 CRDT…]]></description><link>https://www.zxch3n.com/crdt-intro/crdt-intro/</link><guid isPermaLink="false">https://www.zxch3n.com/crdt-intro/crdt-intro/</guid><pubDate>Tue, 21 Dec 2021 22:00:02 GMT</pubDate><content:encoded>&lt;blockquote&gt;&lt;p&gt;&lt;a href=&quot;/crdt-intro/crdt-intro.en&quot;&gt;English version&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:62.65822784810127%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABRNAAAUTQGUyo0vAAACDUlEQVQ4y21Ta08aURDlP/cH9Eu/90P7sWn6iIk12hhrH1apxUjVlCJYEAEFljfL8thddln2wWnObFkUmWRz796Ze+bMnLmx+XyOhQVBIGu/O4bjuLhvjFv3BcFyT4uFQHPYlhOBDjUDnuvDnjhQ7npwpu4j4MV+NakAGmMbrbqGiTlFpdRBLl1Drz0SMLKlb2rP4HvB8nIQgl1nFexuJKF2RiFD07BRKXYEbDycQFN1cbQUDd3WEK7rodscQrntydqo9jExphLD+K3XCRx/yeBw708IyKylXBOJgyvJmkoWcZ4oCP3UaRGfNs/QrPWRSpbw7uUh7m7aUakEvLmqC2D+shYCspSP709hjC3p4/OnOzjYTUkwA0v5JjZfHcv/t53fUckskWf9ni4lL9oQUztjvH3xXXpmmVM8e7KBXz/ysK0Ztt+cYP/DuWSnSJ+3LiJ21sTB31QF8f1LFLJKKKgfhAwzF7f4+TULU7cxGphgEvaukK2jVu6KKGTUbgzgzrwH6h7tpcUfqcxAZqO6jaoKfWRFwTxnWUzKdlTLXdQrqvh8P1Q8Gc/JJESAVI9N5wWWRbV56TqjiABkS8W5siWrjyAZz2OkmUtAZmKZncZAGPIigWYzD77ny1C7rv9osIP/c5g+K0Pr6Q8Hm0Z2pE6GA9XAqq17djTOLSckArzvXDxDirMOZPW5rdv/A5Ig2MSHd6a7AAAAAElFTkSuQmCC&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03&quot; title=&quot;355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03&quot; src=&quot;/static/a1d092aa8bcaf737b8de0e1279057dab/f058b/355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03.png&quot; srcSet=&quot;/static/a1d092aa8bcaf737b8de0e1279057dab/c26ae/355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03.png 158w,/static/a1d092aa8bcaf737b8de0e1279057dab/6bdcf/355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03.png 315w,/static/a1d092aa8bcaf737b8de0e1279057dab/f058b/355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03.png 630w,/static/a1d092aa8bcaf737b8de0e1279057dab/40601/355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03.png 945w,/static/a1d092aa8bcaf737b8de0e1279057dab/78612/355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03.png 1260w,/static/a1d092aa8bcaf737b8de0e1279057dab/e8950/355128528510c6e4115148b6b5beb85ff3307ff6ada56ebdab5d2400abaddb03.png 2000w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;  &lt;/p&gt;&lt;p&gt;CRDT (conflict-free replicated data type) 无冲突复制数据类型，是一种可以在网络中的多台计算机上复制的数据结构，副本可以独立和并行地更新，不需要在副本之间进行协调，并保证不会有冲突发生。&lt;/p&gt;&lt;p&gt;CRDT 常被用在协作软件上，例如多个用户需要共同编辑/读取共享的文档、数据库或状态的场景。在数据库软件，文本编辑软件，聊天软件等都可以用到它。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;#crdt-%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98&quot;&gt;CRDT 解决了什么问题&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E8%B5%B7%E6%BA%90&quot;&gt;起源&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E7%AE%80%E5%8D%95%E7%9A%84-crdt-%E6%A1%88%E4%BE%8B&quot;&gt;简单的 CRDT 案例&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%8E%9F%E7%90%86%E7%AE%80%E4%BB%8B&quot;&gt;原理简介&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#crdt-%E5%92%8C-ot-%E7%9A%84%E5%AF%B9%E6%AF%94&quot;&gt;CRDT 和 OT 的对比&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E5%B7%B2%E8%A7%A3%E5%86%B3%E7%9A%84%E9%97%AE%E9%A2%98--%E7%9B%AE%E5%89%8D%E8%BF%98%E5%AD%98%E5%9C%A8%E7%9A%84%E9%97%AE%E9%A2%98&quot;&gt;已解决的问题 \&amp;amp; 目前还存在的问题&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E7%8E%B0%E5%9C%A8%E5%BC%80%E5%A7%8B%E4%BD%BF%E7%94%A8-crdt&quot;&gt;现在开始使用 CRDT&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;#%E6%89%A9%E5%B1%95%E9%98%85%E8%AF%BB&quot;&gt;扩展阅读&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;crdt-解决了什么问题&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#crdt-%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98&quot; aria-label=&quot;crdt 解决了什么问题 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CRDT 解决了什么问题&lt;/h1&gt;&lt;p&gt;例如多用户在线同时编辑同一篇文档的场景&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:100%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABRNAAAUTQGUyo0vAAACw0lEQVQ4y4VUiU7iUBTt/3/EJBpxFARGjJo4jtGMDO4Le1sUWjYplKVQuvdM7oNiWZxp8tKb917Ou/fccy7n+z5oBV8Qh/fCX/g8iD3PW5xzQdDrjaHr5j/BNj3ouh4qlQ8Mhzrb44LDRqOPUqkJx3GXMvgq+yA2DBut1gDdrgbLcmaAlHK9riKbldcAN8XB33E8VpUkddHpjD4BCaRcbmEyMdkmlbGa5SpnBJBOCygWm9A0A7VaF7Y9ByQQWe6xy6o6ZpxMpxY8L8ylP1+ALPXY46L4gUZDZfff3xWWGDfjwWL8ER8BL5dXZUiyCtd1YVsO7BkTKPNtvL0rLCbunp9ryOXqqFQ6sO05IAWULgEFZTX4KornV8gdnSCbTIG/vsPbQxa5058Q7l7Znc7HEO32gMX9/mTG4apEAk0phTykw30IR4fIJxJ42okg820br7E4+ItLmIa11v2FDpdIDzJ8ySIf2cJjZBcP0QTuv8dwv70D+TQJ8ewMxnSuWW9ZCdwmZ7Q7GmrpP6gcRPC4tYWH3X3c7saQ2YmCj++hkIhDH+sb9cmFN6g51aoCQexAzmTQPIlBSMbAp36glEqhdHyC1sUp+KNDvNwUkS8019zFfWrLh1TrwvV8uJbFeGqcH6MQiyJ/cIDs/h5y0Sj4RBRP8RS0gYbhcIpms79e8nhsQFE0KMoI44kJQzdQFeqoFUSI6TsI17fgf2dQ/nWJSvoGxWcRJtORzzobdhdHmQU+JINLUo8JduNgoGEwp3o00hcepkU2ZCUTIGmIDjVtitFoygDJfvQiyYg6uWo/qopcRVXRpFrIhi7QC6o6wWCgsyzJhmS9TX4OjEDNaLeHrCK6S2thPXIJkTtL3WWARMGqnMINNE2H8U6gNByoMtO0PwcslUkThD7KNDxxNg1YOicAGhLB3UXJ/xv3Xw3YGcfLlPwFuqz10OMmQh4AAAAASUVORK5CYII=&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;多用户编辑&quot; title=&quot;多用户编辑&quot; src=&quot;/static/dd72b43c308694b1853565c1ab6ef774/f058b/22a11d131ecdc2622d427594857658ffea14634328c973a06777089de599970e.png&quot; srcSet=&quot;/static/dd72b43c308694b1853565c1ab6ef774/c26ae/22a11d131ecdc2622d427594857658ffea14634328c973a06777089de599970e.png 158w,/static/dd72b43c308694b1853565c1ab6ef774/6bdcf/22a11d131ecdc2622d427594857658ffea14634328c973a06777089de599970e.png 315w,/static/dd72b43c308694b1853565c1ab6ef774/f058b/22a11d131ecdc2622d427594857658ffea14634328c973a06777089de599970e.png 630w,/static/dd72b43c308694b1853565c1ab6ef774/40601/22a11d131ecdc2622d427594857658ffea14634328c973a06777089de599970e.png 945w,/static/dd72b43c308694b1853565c1ab6ef774/78612/22a11d131ecdc2622d427594857658ffea14634328c973a06777089de599970e.png 1260w,/static/dd72b43c308694b1853565c1ab6ef774/a878e/22a11d131ecdc2622d427594857658ffea14634328c973a06777089de599970e.png 2048w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;  &lt;/p&gt;&lt;p&gt;这个场景要求每个用户看到的内容都是一样的，即使在用户出现冲突编辑后（例如两个用户同时修改标题，两个请求同时到达服务器）也不会产生两个版本，这被称为&lt;strong&gt;一致性&lt;/strong&gt;。（准确地说 CRDT 满足的是最终一致性，见下文详述）&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:100%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABRNAAAUTQGUyo0vAAACo0lEQVQ4y41Uy27bMBD0f/bWAr21p/5cbwUa9JpDgCSuHT/i6C2KEkW9JU8xK0txHAcoAUIkRc7uDHe4OB6PYJ/aND9fu2yXe4ZhmP8tpkEc5yiK+s3Bru3RdT2qspEu87Z/s6fvB6zXPtK0kACLKeLLS4KHB0cA6qqBVhZpYpEqi9yUsKYa56e1TBcCWFUtXFcjigyaphsBmfLhoHB3d0BuKjk8DCOdLC2wW/mIg2zOqixqqNBIdmS130cIguwVkFk9ProzZR4m6ESXGVZFI3OTFrB5BWtr3NwscX/vwJgKu12Etj0BEvn5OZ4FH/pBsiA9AuRZKZ00GWBkdcRy6YlU1HCzCSXgYtShEf3qusX/tONJDs9LcXu7E6nW6wBtewLkgOlSYDZSOjs+Zz61qUx8P5ULYUsSO2p4rd6KsoHaRqhUjq4HTKpkve16WD+FCQ2OVzKe6/C8UKcAbVUjOBQInJ9Y331Bqn4hNx2cTQCrC7ko6jrJNJ1dXFb/OB4pNXWEXH2Fs/kEHXxD6B2wXUU4bEMErobvJLM8M+BldiPoAEJG3h/U+Xd4u8+w+gdUsPzQiu+sd9mSbQx3H8CkewTOb6TqLzxXIX4K0VftO6A3GRpTihezbPw2dYciLa4GsqFB4mrRjk5h7bFPoAuWAH3IDVpbKfDVyhfK/Nc2Lfquky8DTU0nFp6nEYZGzhNcKLPiWUOsJ1qIWa7WvkQt8gpa5QicRL58MOJw9DStp1SOMMzkpZo1ZJqMoJSF1oVQpp1CPxWb0XpPj+6sF4PQCPQ9nbLfxyjLRvpsPbrEcRIB5ma+PDyQaSuZ0cMc09tN3Qo4JSJdgk7MuDbfMnULTk8UM52eLxH+9LCe3yrXCUDqk34z5avP/Qfrsv+sTBj4vIb/AcqoDtCaMeu9AAAAAElFTkSuQmCC&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;CRDT 支持 P2P 地进行同步&quot; title=&quot;CRDT 支持 P2P 地进行同步&quot; src=&quot;/static/588890f957763c2d836bf9a0bf899066/f058b/7a948606a9246f0f632c382370b79519e046e92c8745b000c35675c215456baa.png&quot; srcSet=&quot;/static/588890f957763c2d836bf9a0bf899066/c26ae/7a948606a9246f0f632c382370b79519e046e92c8745b000c35675c215456baa.png 158w,/static/588890f957763c2d836bf9a0bf899066/6bdcf/7a948606a9246f0f632c382370b79519e046e92c8745b000c35675c215456baa.png 315w,/static/588890f957763c2d836bf9a0bf899066/f058b/7a948606a9246f0f632c382370b79519e046e92c8745b000c35675c215456baa.png 630w,/static/588890f957763c2d836bf9a0bf899066/40601/7a948606a9246f0f632c382370b79519e046e92c8745b000c35675c215456baa.png 945w,/static/588890f957763c2d836bf9a0bf899066/78612/7a948606a9246f0f632c382370b79519e046e92c8745b000c35675c215456baa.png 1260w,/static/588890f957763c2d836bf9a0bf899066/a878e/7a948606a9246f0f632c382370b79519e046e92c8745b000c35675c215456baa.png 2048w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;  &lt;/p&gt;&lt;p&gt;CRDT 让用户即使离线也可使用，并在恢复网络后能继续和所有人同步至一致的状态。也可以和其他用户通过 P2P 的方式一起协同编辑。这被称为&lt;strong&gt;分区容错性&lt;/strong&gt;。这让 CRDT 可以很好地支持&lt;strong&gt;去中心化&lt;/strong&gt;的应用：即使没有中心化服务器各端之间也能完成同步。&lt;/p&gt;&lt;h1 id=&quot;起源&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E8%B5%B7%E6%BA%90&quot; aria-label=&quot;起源 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;起源&lt;/h1&gt;&lt;p&gt;CRDT 的正式定义出现在 Marc Shapiro 2011 年的论文 &lt;a href=&quot;https://readpaper.com/paper/1516319412&quot;&gt;Conflict-free replicated data types&lt;/a&gt; 中(而2006 的&lt;a href=&quot;https://doi.org/10.1145%2F1180875.1180916&quot;&gt;Woot&lt;/a&gt;可能是最早的研究)。提出的动机是因为最终一致性(Eventual Consistency) 的冲突解决设计很困难，很少有文章给出设计指导建议，而随意的设计的方案容易出错。所以这篇文章提出了简单的、理论证明的方式来达到最终一致性，也就是 CRDT。&lt;/p&gt;&lt;p&gt;(PS: 其实 Marc Shapiro 在 2007 年就写了一篇 &lt;a href=&quot;https://hal.inria.fr/inria-00177693v2/document&quot;&gt;Designing a commutative replicated data type&lt;/a&gt;，2011 年将 commutative 变成了 conflict-free，在其定义上扩充了 State-based CRDT)&lt;/p&gt;&lt;p&gt;根据 &lt;a href=&quot;https://en.wikipedia.org/wiki/CAP_theorem&quot;&gt;CAP 定理&lt;/a&gt;，对于一个分布式计算系统来说，不可能同时完美地满足以下三点：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;一致性（Consistency）: 每一次读都会收到最近的写的结果或报错；表现起来像是在访问同一份数据&lt;/li&gt;&lt;li&gt;可用性（Availability）: 每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据&lt;/li&gt;&lt;li&gt;分区容错性（Partition tolerance）: 以实际效果而言，分区相当于对通信的时限要求&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;系统如果不能在时限内达成数据一致性，就意味着发生了分区的情况，必须就当前操作在C和A之间做出选择，所以「完美的一致性」与「完美的可用性」是冲突的。&lt;/p&gt;&lt;p&gt;CRDT 不提供「完美的一致性」，它提供了 &lt;a href=&quot;#-%E5%BC%BA%E6%9C%80%E7%BB%88%E4%B8%80%E8%87%B4%E6%80%A7---strong-eventual-consistency-sec&quot;&gt;强最终一致性 Strong Eventual Consistency (SEC)&lt;/a&gt; 。这代表进程 A 可能无法立即反映进程 B 上发生的状态改动，但是当 A B 同步消息之后它们二者就可以恢复一致性，并且不需要解决潜在冲突（CRDT 在数学上就不让冲突发生）。而「强最终一致性」是不与「可用性」和「分区容错性」冲突的，所以 CRDT 同时提供了这三者，提供了很好的 CAP 上的权衡。&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:52.53164556962025%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABRNAAAUTQGUyo0vAAABx0lEQVQoz42TXUvcQBSG90f6EwqCUCpI7UV7o2DZC3sl1CsLFZFVC26tslCqKLV1KUpZupbidmXNfpk0ySaTNMkmM/OWmXw47XrRA8Nkzkmec945JyXOOcQSFo8TjKMEURiDUiZ9eSw7TPi46uMcJRVoGQSdlg6tbSDwo8kPlD2OEiQxlRA1VlJfYozh54/Bvdm5ck4ow3VbR16niOaxv4A+CTHQTLgj/w6kVsDS3WvpaB00oFUv4DW0DJpVWGSNKYzhSD4T5zdGlpdVncPSO/Uu+zh7XEHzRQ31Z2/wcW4TTr1dJCyAxA0kxP5F4Ng+bvu2bI6EZg1iToBPjzawPf0KO0938HLmNbbnK2gu10B1cifZGDpSrgCI6jw3QBiMJdTU3eJOv5X3cfRgDcfldzhaeovD53v4vFjFycN1NJZr4FGSAlvfexh0LQkzDVdKFyABzKVTEuLD1ApOZjdwuXqIxsp7nC/s4my+gvqTLZwvVkFNHyVLJ+h3TAw1Kx0DxWzDg94dgSapnzR7sL9cw7nowDy9gvP1BlHPRjx0wd0wlSyGWcgTSwyz2lkBNHoOaMLwP1bM4b/OiT8kHyHGZCeLpExZme8P3gpBDHChkBgAAAAASUVORK5CYII=&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;CPA&quot; title=&quot;CPA&quot; src=&quot;/static/ccafe0751b5557ae551b634eb9c47e16/f058b/a4858e2a50bc1a2d79722060156e89b0cac5815cf25e8c67e409aa0926280cef.png&quot; srcSet=&quot;/static/ccafe0751b5557ae551b634eb9c47e16/c26ae/a4858e2a50bc1a2d79722060156e89b0cac5815cf25e8c67e409aa0926280cef.png 158w,/static/ccafe0751b5557ae551b634eb9c47e16/6bdcf/a4858e2a50bc1a2d79722060156e89b0cac5815cf25e8c67e409aa0926280cef.png 315w,/static/ccafe0751b5557ae551b634eb9c47e16/f058b/a4858e2a50bc1a2d79722060156e89b0cac5815cf25e8c67e409aa0926280cef.png 630w,/static/ccafe0751b5557ae551b634eb9c47e16/40601/a4858e2a50bc1a2d79722060156e89b0cac5815cf25e8c67e409aa0926280cef.png 945w,/static/ccafe0751b5557ae551b634eb9c47e16/78612/a4858e2a50bc1a2d79722060156e89b0cac5815cf25e8c67e409aa0926280cef.png 1260w,/static/ccafe0751b5557ae551b634eb9c47e16/9ac09/a4858e2a50bc1a2d79722060156e89b0cac5815cf25e8c67e409aa0926280cef.png 2204w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;
&lt;em&gt;CRDT 满足 A + P + Eventual Consistency；是 CAP 下很好的权衡&lt;/em&gt;&lt;/p&gt;&lt;p&gt;(PS: 在 2012 年，CAP 定理的作者 Eric Brewer 写了一篇文章&lt;a href=&quot;https://www.infoq.com/articles/cap-twelve-years-later-how-the-rules-have-changed/&quot;&gt;CAP Twelve Years Later: How the “Rules” Have Changed&lt;/a&gt;，解释了“CAP 特性三选二” 的描述其实具有误导性，实际上 CAP 只禁止了设计空间的很小一部分即存在分区时的完美可用性和一致性；而实际上在 C 和 A 之间的权衡的设计非常灵活，CRDT 就是一个很好的例子。)&lt;/p&gt;&lt;h1 id=&quot;简单的-crdt-案例&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E7%AE%80%E5%8D%95%E7%9A%84-crdt-%E6%A1%88%E4%BE%8B&quot; aria-label=&quot;简单的 crdt 案例 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;简单的 CRDT 案例&lt;/h1&gt;&lt;p&gt;我们可以通过几个简单的例子来大致理解 CRDT 类算法达到 Strong Eventual Consistency 的思路。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Grow-only Counter&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;如何在分布式系统中不加锁地统计一件事情的发生次数呢？&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;/a18b381322866b3752e3f376f6ccd31d/G-Counter.gif&quot;/&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;让每个副本只能递增自己的计数器 =&amp;gt; 不用加锁同步 &amp;amp; 不会发生冲突&lt;/li&gt;&lt;li&gt;每个副本上同时保存着所有其他副本的计数值&lt;/li&gt;&lt;li&gt;发生次数 = 所有副本计数值之和&lt;/li&gt;&lt;li&gt;因为每个副本都只会更新自己的计数值，不会与其他计数器产生冲突，所以该类型在消息同步后便满足一致性&lt;/li&gt;&lt;/ul&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Grow-only Set&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;img src=&quot;/18190698321a05380c6e984d93b77032/G-Set.gif&quot;/&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Grow-only Set 当中的元素是只能增加不能减少的&lt;/li&gt;&lt;li&gt;将两个这样的状态合并就只需要做并集&lt;/li&gt;&lt;li&gt;因为元素只增不减，不存在冲突操作，所以该类型在消息同步后便满足一致性&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;上述两种方法都是 CRDT。他们都满足以下性质&lt;/p&gt;&lt;ul&gt;&lt;li&gt;他们都可以被独立并发地更新，而不需要副本之间进行协调（加锁）&lt;/li&gt;&lt;li&gt;多个更新之间不可能发生冲突&lt;/li&gt;&lt;li&gt;总是可以保证最终一致性&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;原理简介&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%8E%9F%E7%90%86%E7%AE%80%E4%BB%8B&quot; aria-label=&quot;原理简介 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;原理简介&lt;/h1&gt;&lt;p&gt;CRDT 有两种类型：Op-based CRDT 和 State-based CRDT，此处仅介绍 Op-based 的思路。&lt;/p&gt;&lt;p&gt;Op-based CRDT 的思路为：如果两个用户的操作序列是完全一致的，那么最终文档的状态也一定是一致的。所以索性各个用户保存对数据的所有操作(Operations)，用户之间通过同步 Operations 来达到最终一致状态。但我们怎么保证 Op 的顺序是一致的呢，如果有并行的修改操作应该谁先谁后？所以 Op-based CRDT 要求可能并行的 Op 都是可交换的，由此就可以满足最终一致性的要求。&lt;/p&gt;&lt;p&gt;如果想看 State-based CRDT 的原理，以及其他更深入的内容欢迎阅读本系列下一章&lt;a href=&quot;../design-crdt&quot;&gt;如何设计 CRDT&lt;/a&gt;。&lt;/p&gt;&lt;h1 id=&quot;crdt-和-ot-的对比&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#crdt-%E5%92%8C-ot-%E7%9A%84%E5%AF%B9%E6%AF%94&quot; aria-label=&quot;crdt 和 ot 的对比 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CRDT 和 OT 的对比&lt;/h1&gt;&lt;p&gt;CRDT 和 &lt;a href=&quot;https://en.wikipedia.org/wiki/Operational_transformation&quot;&gt;Operation Transformation(OT)&lt;/a&gt; 都可以被用于在线协作型的应用中，二者的差别如下&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;OT&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;CRDT&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;OT 依赖中心化服务器完成协作; &lt;a href=&quot;https://digitalfreepen.com/2018/01/04/operational-transform-hard.html&quot;&gt;如果想要让它在分布式环境中工作就异常困难&lt;/a&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;CRDT 算法可以通过 P2P 的方式完成数据的同步&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;OT 最早的论文于 1989 年提出&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;CRDT 最早的论文出现于 2006 年&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;为保证一致性，OT 的算法设计时的复杂度更高&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;为保证一致性，CRDT 算法设计更简单&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;OT 的设计更容易保留用户意图&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;设计一个保留用户意图的 CRDT 算法更困难&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;OT 不影响文档体积&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;CRDT 文档比原文档数据更大&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;更多相关讨论可看&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.infoq.com/presentations/crdt-distributed-consistency/&quot;&gt;CRDTs and the Quest for Distributed Consistency&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://josephg.com/blog/crdts-are-the-future/&quot;&gt;I was wrong. CRDTs are the future&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;已解决的问题--目前还存在的问题&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%B7%B2%E8%A7%A3%E5%86%B3%E7%9A%84%E9%97%AE%E9%A2%98--%E7%9B%AE%E5%89%8D%E8%BF%98%E5%AD%98%E5%9C%A8%E7%9A%84%E9%97%AE%E9%A2%98&quot; aria-label=&quot;已解决的问题  目前还存在的问题 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;已解决的问题 &amp;amp; 目前还存在的问题&lt;/h1&gt;&lt;blockquote&gt;&lt;p&gt;此部分内容最后更新时间为 2021 年 12 月&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;为什么目前在协作软件中大多数看到的还是应用 OT 算法而不是 CRDT 呢？首先因为 CRDT 这类方法相比 OT 还比较年轻，而且有些难点近几年才被比较好地解决，例如：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;如何让存在冲突的编辑的合并结果尽量符合用户预期 （这个问题上用 OT 设计往往比 CRDT 更容易，CRDT 本身只保证一致性，让合并的结果符合预期是需要专门设计的）&lt;ul&gt;&lt;li&gt;相关论文 &lt;a href=&quot;https://martin.kleppmann.com/papers/interleaving-papoc19.pdf&quot;&gt;Interleaving anomalies in collaborative text editors&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;CRDT Move&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://martin.kleppmann.com/papers/move-op.pdf&quot;&gt;A highly-available move operation for replicated trees&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://martin.kleppmann.com/papers/list-move-papoc20.pdf&quot;&gt;Moving Elements in List CRDTs&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;如何实现高性能的 CRDT&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;而目前在以下方面的研究还有待展开&lt;/p&gt;&lt;ul&gt;&lt;li&gt;CRDT 中常常存在难以回收的墓碑数据，如何才能更好地回收 CRDT 的墓碑？&lt;/li&gt;&lt;li&gt;如何降低更新 CRDT 文档的开销？&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;现在开始使用-crdt&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E7%8E%B0%E5%9C%A8%E5%BC%80%E5%A7%8B%E4%BD%BF%E7%94%A8-crdt&quot; aria-label=&quot;现在开始使用 crdt permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;现在开始使用 CRDT&lt;/h1&gt;&lt;p&gt;你不用自己从头开始设计和实现 CRDTs 算法（CRDT 很容易被实现得很糟糕）。你可以直接基于开源 CRDTs 项目来搭建你的应用例如 &lt;a href=&quot;https://github.com/automerge/automerge&quot;&gt;Automerge&lt;/a&gt;, &lt;a href=&quot;https://github.com/yjs/yjs&quot;&gt;Yjs&lt;/a&gt; 以及我正在研发的 &lt;a href=&quot;https://loro.dev&quot;&gt;Loro&lt;/a&gt;。你可以在&lt;a href=&quot;https://www.loro.dev/docs/performance&quot;&gt;这里&lt;/a&gt;查看它们的性能对比。&lt;/p&gt;&lt;h1 id=&quot;扩展阅读&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E6%89%A9%E5%B1%95%E9%98%85%E8%AF%BB&quot; aria-label=&quot;扩展阅读 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;扩展阅读&lt;/h1&gt;&lt;ul&gt;&lt;li&gt;Marc Shapiro, Nuno Preguiça, Carlos Baquero and Marek Zawirski, &lt;a href=&quot;https://readpaper.com/paper/1516319412&quot;&gt;Conflict-free replicated data types&lt;/a&gt; (2011 正式定义 CRDT 的论文)&lt;/li&gt;&lt;li&gt;Petru Nicolaescu, Kevin Jahns, Michael Derntl and Ralf Klamma, &lt;a href=&quot;https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types&quot;&gt;Near Real-Time Peer-to-Peer Shared Editing on Extensible Data Types&lt;/a&gt; ( Yjs 论文)&lt;/li&gt;&lt;li&gt;Martin Kleppmann, Alastair R. Beresford, &lt;a href=&quot;https://arxiv.org/abs/1608.03960&quot;&gt;A Conflict-Free Replicated JSON Datatype&lt;/a&gt; (Automerge)&lt;/li&gt;&lt;li&gt;Martin Kleppmann, &lt;a href=&quot;https://martin.kleppmann.com/2020/07/06/crdt-hard-parts-hydra.html&quot;&gt;CRDTs The Hard Parts&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Seph, &lt;a href=&quot;https://josephg.com/blog/crdts-go-brrr/&quot;&gt;5000x faster CRDTs: An Adventure in Optimization&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;前往下一章：&lt;a href=&quot;../design-crdt&quot;&gt;如何设计 CRDT 算法&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;../design-crdt&quot;&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:630px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:58.86075949367089%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABRNAAAUTQGUyo0vAAAByUlEQVQoz41Ta1PaQBTl//+JfnE6HduxVqfqOLZWRRGwYHgFaEhIIAbyBJJATLI5nb2WoIzTYWc2u7mz95x77p4tAECWZTS394xlYIzl8V1GgX+WQYS5F1LANue0uvYCoqBiKE2wWj6/IfvfJMDQX8GazBAGETTZRJKkkPsGHssS6vcSFrMwr4ClLE9+s2eMFL0ABhFc26eE8dCiA8swQrehQRQ0iqdJijRlu0mO44SATMOD2NAIMFrFaNUUCFWZ5I9VC4d7tyhddnB1JqBy08XdzzYujmooXrRQK/2BOpi+AHL2XmuEdn2I5oMMf74kUNOYQRJ1PEcxya6XJVSLPTQeZNz+aOPzh2s0fyvot8f4dSoQMAE61gIzN8D1eRNCZUDV8H7oqg2lb7wrjatK4jT/533PL8VzfKru+FMJ3z7eQZWmCPwVSdI1O0/iBLy6kWLi5EsFX/eKlHN2UIXcMzY95AenTx6+75dJ1vAfIL9lLn89jLGDjqDCcwJ0myPqWedRxZPmUM9zQM441V1MdBcjxUL5SoRjzsmTXPquxt74MIioZG5koTog5m3DrhM4wWvf8TVjmzOF9YZ7jL+I143eZt9+nu+R/gVzCpDXGJkojAAAAABJRU5ErkJggg==&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;设计 CRDT&quot; title=&quot;设计 CRDT&quot; src=&quot;/static/4e78d08dad64b835622765444af0c517/f058b/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png&quot; srcSet=&quot;/static/4e78d08dad64b835622765444af0c517/c26ae/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 158w,/static/4e78d08dad64b835622765444af0c517/6bdcf/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 315w,/static/4e78d08dad64b835622765444af0c517/f058b/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 630w,/static/4e78d08dad64b835622765444af0c517/40601/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 945w,/static/4e78d08dad64b835622765444af0c517/78612/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 1260w,/static/4e78d08dad64b835622765444af0c517/4e814/488051dd79b5c0a1ce13dcc71d286687ecd2182eec5fbc9361dcae3da11a63cd.png 3211w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;/&gt;
    &lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Throwable - 在 TypeScript 中类型安全地处理 Error]]></title><description><![CDATA[在 ts/js 中我们一般通过 throw, try..catch 来处理 error， 但是这种方式无法保证类型安全: 一个 function 无法告诉使用者它可能出现的必须要被处理的问题。这很大程度限制了 lib 开发者的表达能力：因为没处理的 throw…]]></description><link>https://www.zxch3n.com/throwable/</link><guid isPermaLink="false">https://www.zxch3n.com/throwable/</guid><pubDate>Sun, 22 Aug 2021 22:00:02 GMT</pubDate><content:encoded>&lt;p&gt;在 ts/js 中我们一般通过 throw, try..catch 来处理 error， 但是这种方式无法保证类型安全: 一个 function 无法告诉使用者它可能出现的必须要被处理的问题。这很大程度限制了 lib 开发者的表达能力：因为没处理的 throw 可能会导致应用崩溃，所以在出现无法处理的情况时直接 &lt;code class=&quot;language-text&quot;&gt;return undefined&lt;/code&gt; 可能是更好的选择。&lt;/p&gt;&lt;p&gt;但是我们可以借鉴 Haskell 和 Rust 当中处理异常的方式。这两门语言都没有提供 try…catch 的方法，而是通过一种特殊的函数返回类型来表达异常。在 Haskell 中用了一种数据结构：&lt;code class=&quot;language-text&quot;&gt;Either&lt;/code&gt;，它在 Rust 中被称为 &lt;code class=&quot;language-text&quot;&gt;Result&lt;/code&gt;。例如下面除法的例子，函数的返回值是 &lt;code class=&quot;language-text&quot;&gt;Result&lt;/code&gt; 类型，而不直接是数字。&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;rust&quot;&gt;&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;x&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;u32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; y&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;u32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;u32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; 
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; y &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Err&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;divZero&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;x &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; y&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Result&lt;/code&gt; 类型不像 throw 出的 Error 会自动向上冒泡，而是将这个冒泡的流程交给开发者进行。Rust &lt;a href=&quot;https://doc.rust-lang.org/std/result/enum.Result.html&quot;&gt;提供了很多 Utils 函数&lt;/a&gt; 来方便上层的程序对其进行处理。&lt;/p&gt;&lt;p&gt;有的小伙伴可能会问，&lt;code class=&quot;language-text&quot;&gt;T | undefined&lt;/code&gt; 或者 &lt;code class=&quot;language-text&quot;&gt;T | Error&lt;/code&gt; 好像也能解决问题？确实能解决类型标注上的问题，但是这样使用起来笨拙很多，会有很多重复性的代码。&lt;/p&gt;&lt;h1 id=&quot;throwable&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#throwable&quot; aria-label=&quot;throwable permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Throwable&lt;/h1&gt;&lt;p&gt;我借鉴 Rust 和 Haskell 实现了 &lt;a href=&quot;https://github.com/zxch3n/throwable&quot;&gt;Throwable&lt;/a&gt;。在概念上 &lt;code class=&quot;language-text&quot;&gt;Throwable&lt;/code&gt; 和 Rust 的 &lt;code class=&quot;language-text&quot;&gt;Result&lt;/code&gt; 是一样的， 改称为 Throwable 主要是考虑理解成本。&lt;/p&gt;&lt;h2 id=&quot;使用方式&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F&quot; aria-label=&quot;使用方式 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;使用方式&lt;/h2&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;Ok&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Err&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Throwable&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#x27;@typ3/throwable&amp;#x27;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// in deno&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;Ok&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Err&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Throwable&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#x27;https://deno.land/x/throwable@v0&amp;#x27;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Throwable&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#x27;invalid&amp;#x27;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ans &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#x27;{&amp;#x27;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Rather than `throw new Error()`&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Err&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#x27;invalid&amp;#x27;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ans&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&quot;使用对比&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E4%BD%BF%E7%94%A8%E5%AF%B9%E6%AF%94&quot; aria-label=&quot;使用对比 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;使用对比&lt;/h2&gt;&lt;p&gt;对于 Throw 的方案来说它有什么更方便的地方呢？假设我们有以下需求&lt;/p&gt;&lt;ul&gt;&lt;li&gt;给一个 JSON 文件路径列表，读取解析并返回其中 name 字段的值&lt;/li&gt;&lt;li&gt;如果解析错误则跳过&lt;/li&gt;&lt;li&gt;如果文件不存在则尝试增加后缀 &lt;code class=&quot;language-text&quot;&gt;.bk&lt;/code&gt;&lt;/li&gt;&lt;li&gt;如果出现文件不存在 / 解析错误之外的错误，则需要将 Error 抛出&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;传统方式的实现&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E4%BC%A0%E7%BB%9F%E6%96%B9%E5%BC%8F%E7%9A%84%E5%AE%9E%E7%8E%B0&quot; aria-label=&quot;传统方式的实现 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;传统方式的实现&lt;/h3&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NotExistsError&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ParseError&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;readJsonFile&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Object&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;readName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; shouldRetry&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;readJsonFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e &lt;span class=&quot;token keyword&quot;&gt;instanceof&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NotExistsError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;shouldRetry&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;readName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#x27;.bk&amp;#x27;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e &lt;span class=&quot;token keyword&quot;&gt;instanceof&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ParseError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; 
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getValidNames&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;paths&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;paths&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;readName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;x &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; x &lt;span class=&quot;token operator&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&quot;throwable-的实现&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#throwable-%E7%9A%84%E5%AE%9E%E7%8E%B0&quot; aria-label=&quot;throwable 的实现 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Throwable 的实现&lt;/h3&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;MThrowable&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Throwable&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#x27;notExists&amp;#x27;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#x27;parseError&amp;#x27;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; msg&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;readJsonFile&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Object&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;MThrowable&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;readName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;MThrowable&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;readJsonFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;error &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#x27;notExists&amp;#x27;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;readJsonFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#x27;.bk&amp;#x27;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;x &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; x&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getValidNames&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;paths&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;paths&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;readName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;x &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; x&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/zxch3n/throwable&quot;&gt;Throwable 项目地址&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Comlink 源码解析]]></title><description><![CDATA[Web Workers 简介 Web Worker 为 Web 内容在新线程中运行脚本提供了一种简单的方法，线程可以与主进程并行而避免阻塞渲染和交互。对于计算量大，容易造成页面卡顿（耗时 > 16ms）的任务我们可以考虑将它们移至 Worker 进行计算。 根据 CanIUse…]]></description><link>https://www.zxch3n.com/comlink/</link><guid isPermaLink="false">https://www.zxch3n.com/comlink/</guid><pubDate>Thu, 20 May 2021 22:00:02 GMT</pubDate><content:encoded>&lt;h1 id=&quot;web-workers-简介&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#web-workers-%E7%AE%80%E4%BB%8B&quot; aria-label=&quot;web workers 简介 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Web Workers 简介&lt;/h1&gt;&lt;p&gt;Web Worker 为 Web 内容在新线程中运行脚本提供了一种简单的方法，线程可以与主进程并行而避免阻塞渲染和交互。对于计算量大，容易造成页面卡顿（耗时 &amp;gt; 16ms）的任务我们可以考虑将它们移至 Worker 进行计算。&lt;/p&gt;&lt;p&gt;根据 CanIUse &lt;a href=&quot;https://caniuse.com/webworkers&quot;&gt;Web Workers 的覆盖率&lt;/a&gt;已经到了 98%。有人会担心和 Worker 通信造成的卡顿，可参考该 &lt;a href=&quot;https://github.com/zxch3n/js-performance&quot;&gt;Benchmark&lt;/a&gt;&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;通信内容&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;耗时 MacBook Pro (13-inch, M1, 2020) - Chrome&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;耗时 Huawei Mate 10 Pro - Chrome&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;10B 字符串&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;5us&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;5us&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;1MB 字符串&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;200us&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;3ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;10MB 字符串&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;2ms&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;35ms&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;简单通信就只需要 5 微秒左右，而大字符串的传输也接近内存拷贝速度（如果是结构化数据，在这个数据量上可能更多耗时会在 JSON 的解析上）。大家可以根据自己的通信频繁程度，通信内容大小判断是否适合通过 Web Workers 进行优化。&lt;/p&gt;&lt;p&gt;虽然 Web Worker 很好，在平常却常常容易因为在通信上处理上过于麻烦，打包不方便等原因不怎么使用它。&lt;/p&gt;&lt;p&gt;Comlink 解决了其中通信的问题，它通过 Proxy 让你几乎可以忽略其中的通信细节，极大降低了 Worker 的维护成本。&lt;/p&gt;&lt;h1 id=&quot;comlink-介绍&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#comlink-%E4%BB%8B%E7%BB%8D&quot; aria-label=&quot;comlink 介绍 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;a href=&quot;https://github.com/GoogleChromeLabs/comlink&quot;&gt;Comlink&lt;/a&gt; 介绍&lt;/h1&gt;&lt;p&gt;&lt;img src=&quot;https://api2.mubu.com/v3/document_image/732e104a-dd72-4b4d-91e6-5d4757c8fda9-5110239.jpg&quot;/&gt;&lt;/p&gt;&lt;h2 id=&quot;使用案例---通过-comlink-实现简单的插入和查询&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E4%BD%BF%E7%94%A8%E6%A1%88%E4%BE%8B---%E9%80%9A%E8%BF%87-comlink-%E5%AE%9E%E7%8E%B0%E7%AE%80%E5%8D%95%E7%9A%84%E6%8F%92%E5%85%A5%E5%92%8C%E6%9F%A5%E8%AF%A2&quot; aria-label=&quot;使用案例   通过 comlink 实现简单的插入和查询 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;使用案例 - 通过 Comlink 实现简单的插入和查询&lt;/h2&gt;&lt;p&gt;假如有以下场景的需求&lt;/p&gt;&lt;ul&gt;&lt;li&gt;有几十万条数据，每条数据有大约一百个字符&lt;/li&gt;&lt;li&gt;数据会不停地发生变动，比如新增一条数据/删除一段数据&lt;/li&gt;&lt;li&gt;用户需要在这些数据中完成搜索&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;在前端中实现这项功能，如果不通过 Worker 就需要自己完成时间分片来避免阻塞 UI。并且如果主进程此时还有其他类似的繁忙任务在进行就很容易产生卡顿，就需要考虑引入 Scheduler 的模块进行优先级划分。通过 Worker 就可以避免这些麻烦，且有更高的搜索效率。&lt;/p&gt;&lt;p&gt;下面我们通过两种不同的方式，实现在 Worker 上的简单插入和查询。&lt;/p&gt;&lt;h3 id=&quot;直接通过-web-worker-api-的方式实现这个需求&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E7%9B%B4%E6%8E%A5%E9%80%9A%E8%BF%87-web-worker-api-%E7%9A%84%E6%96%B9%E5%BC%8F%E5%AE%9E%E7%8E%B0%E8%BF%99%E4%B8%AA%E9%9C%80%E6%B1%82&quot; aria-label=&quot;直接通过 web worker api 的方式实现这个需求 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;直接通过 Web Worker API 的方式实现这个需求：&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Main.js&lt;/strong&gt;&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 通过原生 API 实现 Worker 上插入和查询&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; worker &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Worker&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;./normal/worker.js&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; _id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 需要通过 id 标识一个请求消息，从而能够知道 response 对应的请求&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; _id&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 通过 id 创建 response promise&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createPromise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;listener&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        worker&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;removeEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;message&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; listener&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    worker&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;message&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; listener&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; promise &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createPromise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  worker&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; s&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;insert&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; promise&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;search&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; promise &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createPromise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  worker&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;search&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; query&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; promise&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Worker.js&lt;/strong&gt;&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/** @type {{id: string, str: string}[]} */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; arr &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function-variable function&quot;&gt;onmessage&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;insert&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    arr&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// 返回中带上请求的 id&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;search&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/** @type {typeof arr} */&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ans &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    arr&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;x&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;str&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        ans&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;x&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// 返回中带上请求的 id&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr/&gt;&lt;h3 id=&quot;通过-comlink-实现该需求&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E9%80%9A%E8%BF%87-comlink-%E5%AE%9E%E7%8E%B0%E8%AF%A5%E9%9C%80%E6%B1%82&quot; aria-label=&quot;通过 comlink 实现该需求 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;通过 Comlink 实现该需求：&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;main.js&lt;/strong&gt;&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 通过 Comlink 实现 Worker 上插入和查询&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; Comlink &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;https://unpkg.com/comlink/dist/esm/comlink.mjs&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; DataProcessor &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Comlink&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;wrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Worker&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;./comlink/worker.js&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; processor &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DataProcessor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; processor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;search&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; processor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;search&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;worker.js&lt;/strong&gt;&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;importScripts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;https://unpkg.com/comlink/dist/umd/comlink.js&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DataProcessor&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  arr &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;s&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;arr&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;s&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;search&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ans &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; value &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;arr&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;str&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        ans&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; ans&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

Comlink&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;expose&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;DataProcessor&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&quot;对比二者我们可以发现&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%AF%B9%E6%AF%94%E4%BA%8C%E8%80%85%E6%88%91%E4%BB%AC%E5%8F%AF%E4%BB%A5%E5%8F%91%E7%8E%B0&quot; aria-label=&quot;对比二者我们可以发现 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;对比二者我们可以发现：&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;通过 Comlink 实现的 Worker 的代码和普通代码几乎无差异，对通信过程无感知&lt;/li&gt;&lt;li&gt;直接通过 Web Worker 实现需要自己管理繁琐的通信过程。如果没有封装自己的 Worker 管理模块则 Worker 内的复杂度很难维护&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;除了上面的好处之外，通过 Comlink 我们&lt;strong&gt;甚至可以传递函数&lt;/strong&gt;，例如&lt;/p&gt;&lt;p&gt;&lt;strong&gt;main.js&lt;/strong&gt;&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;callback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; processor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Comlink&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;proxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;callback&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#x27;0&amp;#x27;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#x27;hello&amp;#x27;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#x27;1&amp;#x27;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#x27;value&amp;#x27;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; x&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;str&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toUpperCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; 
  &lt;span class=&quot;token comment&quot;&gt;// OUTPUT: [&amp;#x27;HELLO&amp;#x27;, &amp;#x27;VALUE&amp;#x27;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;worker.js&lt;/strong&gt;&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DataProcessor&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;callback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; Promise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;arr&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;callback&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;为什么通过 Comlink 还能够在 Worker 和 主进程间传递函数呢？这个函数甚至还能带有原进程的闭包，这是如何做到的呢？请看下一节详解&lt;/p&gt;&lt;h1 id=&quot;原理解析&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90&quot; aria-label=&quot;原理解析 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;原理解析&lt;/h1&gt;&lt;h2 id=&quot;前置知识---channel-messaging-api-️&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%89%8D%E7%BD%AE%E7%9F%A5%E8%AF%86---channel-messaging-api-%EF%B8%8F&quot; aria-label=&quot;前置知识   channel messaging api ️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;前置知识 - Channel Messaging API ✉️&lt;/h2&gt;&lt;p&gt;为了理解 Comlink 的基本原理我们首先要认识 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Channel_Messaging_API&quot;&gt;Channel Messaging API&lt;/a&gt;。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Channel Messaging API 允许两个不同的脚本运行在同一个文档的不同浏览器上下文（Browsing Context, 比如两个 iframe，或者文档主体和一个 iframe，使用 SharedWorker 的两个文档，或者两个 worker）来直接通讯，在每端使用一个端口（port）通过双向频道（channel）向彼此传递消息。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;new MessageChannel()&lt;/code&gt; 返回 &lt;code class=&quot;language-text&quot;&gt;port1&lt;/code&gt; &lt;code class=&quot;language-text&quot;&gt;port2&lt;/code&gt;, 二者之间通过 postMessage / onmessage 可以进行通信。通过 &lt;code class=&quot;language-text&quot;&gt;postMessage&lt;/code&gt; 的方式我们能够将 port 转移到其他的 Browsing Context 上（转移之后它们就从原来的上下文中消失了）。 &lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; channel &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;MessageChannel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; iframe &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#x27;iframe&amp;#x27;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Listen for messages on port1&lt;/span&gt;
channel&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;port1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;onmessage&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;onMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Transfer port2 to the iframe&lt;/span&gt;
iframe&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;contentWindow&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;#x27;Hello from the main page!&amp;#x27;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;#x27;*&amp;#x27;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;channel&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;port2&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;为什么要通过 Channel Messaging 进行通信，而不是直接使用 Worker 的 message event 和 postMessage 呢？&lt;/p&gt;&lt;ol&gt;&lt;li&gt;通过 Channel Messaging 我们能让 Worker 和 Worker 直接通信，而不需要经过主进程&lt;/li&gt;&lt;li&gt;新建 Channel 可以避免消息冲突。例如 Comlink 的实现中就会新建 port 来 expose 不同的对象&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;-comlink-是如何实现的&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#-comlink-%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84&quot; aria-label=&quot; comlink 是如何实现的 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🕶 Comlink 是如何实现的&lt;/h2&gt;&lt;p&gt;(本文的摘录的源码中过滤了不重要的细节，和真实源码略有出入)&lt;/p&gt;&lt;p&gt;下面将针对几个问题逐个剖析&lt;/p&gt;&lt;ul&gt;&lt;li&gt;如何通信&lt;ul&gt;&lt;li&gt;如何向”传递“一个函数？（&lt;code class=&quot;language-text&quot;&gt;Comlink.proxy&lt;/code&gt;  是如何实现的）&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;如何做到无感知的操作 worker 的对象的（只需要加 await 前缀 🧐）&lt;ul&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;await workerObject.value&lt;/code&gt; 就能够拿到 worker 的 obj 的 value&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;workerObject.value = 123&lt;/code&gt; 就能直接赋值给 worker 的 &lt;code class=&quot;language-text&quot;&gt;workerObject.value&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;await new WorkerConstructor()&lt;/code&gt; 就创建一个对象 &lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;如何通信&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%A6%82%E4%BD%95%E9%80%9A%E4%BF%A1&quot; aria-label=&quot;如何通信 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;如何通信&lt;/h3&gt;&lt;p&gt;在 Comlink 的实现中，主动发送消息时都通过 &lt;code class=&quot;language-text&quot;&gt;requestResponseMessage&lt;/code&gt; 函数发送，而回复消息时直接通过 &lt;code class=&quot;language-text&quot;&gt;ep.postMessage&lt;/code&gt;，为了表示回复的是哪一个消息所以需要带上额外的 id 标识。 &lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;requestResponseMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  ep&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Endpoint&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  msg&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Message&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  transfers&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Transferable&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;WireValue&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resolve&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 随机生成一个 uuid， 回复消息时需要带上这个 id&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generateUUID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; 
    ep&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;message&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ev&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MessageEvent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// 消息中的 id 不匹配代表回复的不是此次请求&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;ev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;ev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; ev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      ep&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;removeEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;message&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; l &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ep&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;start&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      ep&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    ep&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;msg &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; transfers&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&quot;如何在进程间传递数据&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%A6%82%E4%BD%95%E5%9C%A8%E8%BF%9B%E7%A8%8B%E9%97%B4%E4%BC%A0%E9%80%92%E6%95%B0%E6%8D%AE&quot; aria-label=&quot;如何在进程间传递数据 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;如何在进程间传递数据&lt;/h4&gt;&lt;p&gt;在 &lt;code class=&quot;language-text&quot;&gt;Comlink.expose&lt;/code&gt; &lt;code class=&quot;language-text&quot;&gt;Comlink.createProxy&lt;/code&gt; 的代码中我们都会看到 &lt;code class=&quot;language-text&quot;&gt;fromWireValue&lt;/code&gt; &lt;code class=&quot;language-text&quot;&gt;toWireValue&lt;/code&gt; 两个函数的身影。例如&lt;/p&gt;&lt;ul&gt;&lt;li&gt;在 setter 中，就会将原始数据 &lt;code class=&quot;language-text&quot;&gt;toWireValue&lt;/code&gt; 封装之后再通过 postMessage 发送&lt;/li&gt;&lt;li&gt;接收方收到消息后，会将数据通过 &lt;code class=&quot;language-text&quot;&gt;fromWireValue&lt;/code&gt; 再转换为可操作的原始数据&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;为什么需要这样的操作呢？直接通过默认的通信方法把 value 塞过去不就好了吗？&lt;/p&gt;&lt;p&gt;这就涉及了 Comlink 的高级 feature：支持跨进程“传递”带原进程闭包的函数/带有这种函数的对象。例如上文的例子中我们可以 &lt;code class=&quot;language-text&quot;&gt;const processor = await new DataProcessor()&lt;/code&gt; 来创建一个来自于 Worker 的对象。其中 &lt;code class=&quot;language-text&quot;&gt;DataProcessor&lt;/code&gt; 是定义于 Worker 上的 constructor，&lt;code class=&quot;language-text&quot;&gt;processor&lt;/code&gt; 也是存在 Worker 进程上，而主进程拿到的只是操作 Worker 中的 &lt;code class=&quot;language-text&quot;&gt;processor&lt;/code&gt; 的本体的句柄：&lt;code class=&quot;language-text&quot;&gt;processor.search&lt;/code&gt; &lt;code class=&quot;language-text&quot;&gt;processor.insert&lt;/code&gt; 等操作都是在 worker 上运行的, 查询/插入的数据也都处于 Worker 进程上。&lt;/p&gt;&lt;p&gt;toWireValue &amp;amp; fromWireValue 是如何做成这样的事的呢？首先让我们来回顾以下几个要点：&lt;/p&gt;&lt;ol&gt;&lt;li&gt;首先我们定义好消息的传输终端 &lt;code class=&quot;language-text&quot;&gt;Endpoint&lt;/code&gt; 类型。它能够通过 postMessage 发送消息，包含了 Worker 和 &lt;strong&gt;Chanel Messaging API 中的 port&lt;/strong&gt;&lt;/li&gt;&lt;/ol&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Endpoint&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;EventSource&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; transfer&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Transferable&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  start&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;EventSource&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    listener&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; EventListenerOrEventListenerObject&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    options&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;removeEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    listener&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; EventListenerOrEventListenerObject&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    options&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start=&quot;2&quot;&gt;&lt;li&gt;我们知道 Comlink 中有一个 &lt;code class=&quot;language-text&quot;&gt;expose&lt;/code&gt; 函数, 让一个 object 能够被 Endpoint 上收到的消息所控制&lt;/li&gt;&lt;/ol&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;expose&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ep&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Endpoint &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; self &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  ep&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;message&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;callback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ev&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MessageEvent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start=&quot;3&quot;&gt;&lt;li&gt;我们也知道 Comlink 中有一个 &lt;code class=&quot;language-text&quot;&gt;wrap&lt;/code&gt; 函数, 能够封装一个代理，读取/控制被 &lt;code class=&quot;language-text&quot;&gt;expose&lt;/code&gt; 暴露到 endpoint 的对象。 &lt;/li&gt;&lt;/ol&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;wrap&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ep&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Endpoint&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; target&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Remote&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;createProxy&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; target&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start=&quot;4&quot;&gt;&lt;li&gt;在 &lt;code class=&quot;language-text&quot;&gt;new MessageChannel()&lt;/code&gt; 后我们得到的两个 port，两个 port 之间是能够通过 &lt;code class=&quot;language-text&quot;&gt;postMessage&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;addEventListener(&amp;#x27;message&amp;#x27;, (ev) =&amp;gt; {...})&lt;/code&gt; 进行通信的。且在上文的 Channel Messaging API 的介绍中提到了，port 是能够被转移给另外一个进程的。&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;结合以上四点，如果我们在进程 A 中想把一个带闭包的函数 / 带这样的函数的对象 &lt;code class=&quot;language-text&quot;&gt;fn&lt;/code&gt; 传递给进程 B，只需要&lt;/p&gt;&lt;ul&gt;&lt;li&gt;在进程 A &lt;code class=&quot;language-text&quot;&gt;expose(fn, port1)&lt;/code&gt; &lt;/li&gt;&lt;li&gt;在进程 B &lt;code class=&quot;language-text&quot;&gt;const handler = wrap(port2)&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;从而在进程 B 中就能通过 &lt;code class=&quot;language-text&quot;&gt;handler&lt;/code&gt; 去做需要的处理了。例如&lt;/p&gt;&lt;ul&gt;&lt;li&gt;在 B 中直接调用这个函数 &lt;code class=&quot;language-text&quot;&gt;handler()&lt;/code&gt;&lt;/li&gt;&lt;li&gt;这个调用操作的行为就会被转发到 A 进程中&lt;/li&gt;&lt;li&gt;A 执行 &lt;code class=&quot;language-text&quot;&gt;fn()&lt;/code&gt; &lt;/li&gt;&lt;li&gt;返回值被异步返回给 B&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;这个设计很巧妙的一点就是能够&lt;strong&gt;递归地利用 wrap &amp;amp; expose 的能力&lt;/strong&gt;。例如&lt;/p&gt;&lt;ul&gt;&lt;li&gt;在 B 进程中 &lt;code class=&quot;language-text&quot;&gt;handler.value = callback&lt;/code&gt;，其中 callback 是一个函数&lt;/li&gt;&lt;li&gt;这个 set 操作会被同步到进程 A，所以这个 callback 也需要被包装&lt;/li&gt;&lt;li&gt;这个 callback 会在进程 B 上被 &lt;code class=&quot;language-text&quot;&gt;expose(callback, port1)&lt;/code&gt;&lt;/li&gt;&lt;li&gt;port2 会被传递到进程 A，在进程 A 上被 &lt;code class=&quot;language-text&quot;&gt;wrap(port2)&lt;/code&gt;，得到进程 B 的 callback 的 handler&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;从而 toWireValue / fromWireValue 的实现就不难推测了：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;对于基本类型（plain object，string，number），直接通过默认的 worker 方式传递即可&lt;/li&gt;&lt;li&gt;对于需要被代理的对象（例如带闭包的函数），就需要通过新建 MessageChannel 传递 port，进行 expose &amp;amp; wrap 的方式进行&lt;ul&gt;&lt;li&gt;在默认情况下不会代理函数，需要通过 &lt;code class=&quot;language-text&quot;&gt;Comlink.proxy(callback)&lt;/code&gt; 包装的函数才会被 expose &amp;amp; wrap 的方式传递到另外一个进程&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/*
 * 在传递一个值时（setter, 函数参数传递, 传递函数返回值）
 * 会在原进程通过 toWireValue 封装, 
 * 接收消息的进程通过 fromWireValue 解析
 */&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;toWireValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;WireValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Transferable&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; handler&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; transferHandlers&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;handler&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;canHandle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;serializedValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; transferables&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; handler&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;serialize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; WireValueType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;HANDLER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; serializedValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        transferables&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; WireValueType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;RAW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    transferCache&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fromWireValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; WireValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; WireValueType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;HANDLER&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; transferHandlers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;deserialize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; WireValueType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;RAW&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; transferHandlers &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Map&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  TransferHandler&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;unknown&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;unknown&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;proxy&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; proxyTransferHandler&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; proxyTransferHandler&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TransferHandler&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;object&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; MessagePort&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 只有带了 proxyMarker 的 obj 才会用 proxy 的方式封装&lt;/span&gt;
  canHandle&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;val&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; val &lt;span class=&quot;token keyword&quot;&gt;is&lt;/span&gt; ProxyMarked &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;isObject&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;val&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;val &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; ProxyMarked&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;proxyMarker&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;serialize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/**
      * 新建 MessageChanel
      * 在原进程 expose(obj, port1)
      * 在接收消息的进程 wrap(port2)
      */&lt;/span&gt; 
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; port1&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; port2 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;MessageChannel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;expose&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; port1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;port2&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;port2&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;deserialize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;port&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    port&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;wrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;port&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&quot;如何做到无感知地操作-worker-的对象的&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%A6%82%E4%BD%95%E5%81%9A%E5%88%B0%E6%97%A0%E6%84%9F%E7%9F%A5%E5%9C%B0%E6%93%8D%E4%BD%9C-worker-%E7%9A%84%E5%AF%B9%E8%B1%A1%E7%9A%84&quot; aria-label=&quot;如何做到无感知地操作 worker 的对象的 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;如何做到无感知地操作 Worker 的对象的&lt;/h3&gt;&lt;p&gt;你可能已经猜到是通过 Proxy 实现的。如果你对 Proxy 不熟可以先看看 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy&quot;&gt;Proxy 的 MDN 文档&lt;/a&gt;。&lt;/p&gt;&lt;p&gt;但是它又是如何通过 Proxy 完成这样的代理的呢？&lt;/p&gt;&lt;p&gt;在 Worker 中我们通过 &lt;code class=&quot;language-text&quot;&gt;Comlink.expose(obj)&lt;/code&gt; 选择暴露一个对象，而在主进程中我们通过 &lt;code class=&quot;language-text&quot;&gt;Comlink.wrap(worker)&lt;/code&gt; 获取到这个对象。&lt;/p&gt;&lt;p&gt;所以可以推测出 &lt;code class=&quot;language-text&quot;&gt;wrap&lt;/code&gt; 操作返回了一个 Proxy 对象，将操作变为对 worker 的 postMessage，而 &lt;code class=&quot;language-text&quot;&gt;expose&lt;/code&gt; 则增加了 &lt;code class=&quot;language-text&quot;&gt;message&lt;/code&gt; 事件的 listener，将收到的消息转为操作。&lt;/p&gt;&lt;p&gt;我们可以通过不同的操作类型的实现具体展开&lt;/p&gt;&lt;h4 id=&quot;getter-的实现&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#getter-%E7%9A%84%E5%AE%9E%E7%8E%B0&quot; aria-label=&quot;getter 的实现 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Getter 的实现&lt;/h4&gt;&lt;p&gt;Comlink.wrap 的实现是直接基于 createProxy 的&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;wrap&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ep&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Endpoint&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; target&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Remote&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;createProxy&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; target&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;createProxy 的实现则是封装了一个 Proxy&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;createProxy&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  ep&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Endpoint&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;symbol&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  target&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;object&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Remote&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; isProxyReleased &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; proxy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Proxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/**
      * proxy.a 会调用 get 函数, 此时的函数参数：
      * 
      * _target 等于 target
      * prop    等于 &amp;#x27;a&amp;#x27;
      */&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; prop&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// 当调用 await proxy 时，prop 等于 &amp;#x27;then&amp;#x27;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prop &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;then&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; proxy &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; r &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;requestResponseMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MessageType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fromWireValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;r&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// 递归创建 Proxy，将 prop 推入 path 的末尾&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createProxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; prop&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; proxy &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;此处比较神奇的是 &lt;code class=&quot;language-text&quot;&gt;if (prop === &amp;#x27;then&amp;#x27;)&lt;/code&gt; 条件语句下的处理，为什么这样的处理能够让 &lt;code class=&quot;language-text&quot;&gt;await proxy.value&lt;/code&gt; 就能够直接拿到 worker 当中的值呢？这就要从 &lt;code class=&quot;language-text&quot;&gt;await xxx&lt;/code&gt; 在 Proxy 当中的处理说起。&lt;/p&gt;&lt;p&gt;根据 ECMAScript® 2022 Language Specification 中 &lt;a href=&quot;https://tc39.es/ecma262/#await&quot;&gt;await&lt;/a&gt; 的描述:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;await value&lt;/code&gt; 在内部实现中会变成 &lt;code class=&quot;language-text&quot;&gt;await Promise.resolve(value)&lt;/code&gt;&lt;/li&gt;&lt;li&gt;而 &lt;a href=&quot;https://tc39.es/ecma262/#sec-promise-resolve-functions&quot;&gt;Promise.resolve 的处理中&lt;/a&gt; 则会获取 &lt;code class=&quot;language-text&quot;&gt;value.then&lt;/code&gt; 的值，如果它是一个函数则会通过它创建一个 Promise Job。&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;所以下面这个例子中 &lt;code class=&quot;language-text&quot;&gt;ans&lt;/code&gt; 等于 100&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ans &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function-variable function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resolve&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; reject&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// ans = 100&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;从而在 &lt;code class=&quot;language-text&quot;&gt;await proxy&lt;/code&gt; 时会触发 Proxy 的 getter，获取的属性等于 &lt;code class=&quot;language-text&quot;&gt;&amp;#x27;then&amp;#x27;&lt;/code&gt;。 &lt;code class=&quot;language-text&quot;&gt;await proxy.a&lt;/code&gt; 在 proxy 的 getter 中最终会走到下面这段代码&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; r &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;requestResponseMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MessageType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// path = [&amp;#x27;a&amp;#x27;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fromWireValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 解析 Response 的返回值&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;r&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 该返回值会被用于创建 Promise Job&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个发送的消息被 Worker 收到后的处理被写在了 &lt;code class=&quot;language-text&quot;&gt;Comlink.expose&lt;/code&gt; 函数中:&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;expose&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ep&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Endpoint &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; self &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  ep&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;message&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;callback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ev&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MessageEvent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; path &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data
    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rawValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; prop&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; obj&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;prop&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; obj&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; MessageType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;GET&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            returnValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; rawValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;wireValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; transferables&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;toWireValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;returnValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    ep&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;wireValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; id &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; transferables&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&quot;setter--function-call--new-的实现&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#setter--function-call--new-%E7%9A%84%E5%AE%9E%E7%8E%B0&quot; aria-label=&quot;setter  function call  new 的实现 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Setter / Function Call / new 的实现&lt;/h4&gt;&lt;p&gt;了解了 getter 的实现后，剩下的几种操作的实现就是类似的：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;createProxy 中代理操作，将操作参数 &lt;code class=&quot;language-text&quot;&gt;toWireValue&lt;/code&gt; 后发送到 worker&lt;/li&gt;&lt;li&gt;worker 接收到操作，进行对应处理后返回所需要的值&lt;/li&gt;&lt;li&gt;原进程收到消息，将消息内容 &lt;code class=&quot;language-text&quot;&gt;fromWireValue&lt;/code&gt; 处理后返回给调用方（通过 Promise 封装） &lt;/li&gt;&lt;/ul&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;createProxy&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  ep&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Endpoint&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;symbol&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  target&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;object&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Remote&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; isProxyReleased &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; proxy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Proxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; prop&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rawValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; transferables&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;toWireValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rawValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;requestResponseMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        ep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MessageType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;SET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; prop&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        transferables
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fromWireValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; _thisArg&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rawArgumentList&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// 多参数的 toWireValue&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;argumentList&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; transferables&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;processArguments&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rawArgumentList&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;requestResponseMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        ep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MessageType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;APPLY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          argumentList&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        transferables
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fromWireValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;construct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rawArgumentList&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// 多参数的 toWireValue&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;argumentList&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; transferables&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;processArguments&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rawArgumentList&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;requestResponseMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        ep&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MessageType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CONSTRUCT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          argumentList&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        transferables
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fromWireValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; proxy &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Comlink.expose&lt;/code&gt; 中的相关实现&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;expose&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ep&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Endpoint &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; self &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  ep&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;quot;message&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;callback&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ev&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MessageEvent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; path &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data
    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; parent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; prop&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; obj&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;prop&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; obj&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rawValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; prop&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; obj&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;prop&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; obj&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; MessageType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;SET&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// value 有可能是 port，需要通过 fromWireValue 处理&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// 详情见上文 「如何在进程间传递数据」&lt;/span&gt;
            parent&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fromWireValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ev&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            returnValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; MessageType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;APPLY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            returnValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rawValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;parent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; argumentList&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; MessageType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CONSTRUCT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;rawValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;argumentList&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// 通过 proxy 标记这个 value 需要被 expose &amp;amp; wrap 封装&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// 详情见上文 「如何在进程间传递数据」&lt;/span&gt;
            returnValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;proxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;wireValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; transferables&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;toWireValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;returnValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    ep&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;wireValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; id &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; transferables&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content:encoded></item><item><title><![CDATA[Haskell 的无穷数组]]></title><description><![CDATA[Haskell是一种标准化的，通用的纯函数式编程语言，有惰性求值和强静态类型。因为有惰性求值，所以在 Haskell 中你可以创建一个无限数组。例如 0.. , 创建一个 0, 1, 2... 不断递增的数组 repeat 0, 创建一个全是 0 长度为正无穷的数组 let a…]]></description><link>https://www.zxch3n.com/haskell-lazy-array-in-js/</link><guid isPermaLink="false">https://www.zxch3n.com/haskell-lazy-array-in-js/</guid><pubDate>Sat, 01 May 2021 22:40:32 GMT</pubDate><content:encoded>&lt;p&gt;Haskell是一种标准化的，通用的纯函数式编程语言，有惰性求值和强静态类型。因为有惰性求值，所以在 Haskell 中你可以创建一个无限数组。例如&lt;/p&gt;&lt;ul&gt;&lt;li&gt;[0..], 创建一个 0, 1, 2… 不断递增的数组&lt;/li&gt;&lt;li&gt;repeat 0, 创建一个全是 0 长度为正无穷的数组&lt;/li&gt;&lt;li&gt;let a = 1:2:a&lt;ul&gt;&lt;li&gt;创建一个数组 a，第一个元素是 1，第二个元素是2，剩下来的是 a （没错，可以递归定义）&lt;/li&gt;&lt;li&gt;得到一个 [1, 2, 1, 2, 1, 2…] 无限重复的数组&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h1 id=&quot;用无穷数组解序列问题&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E7%94%A8%E6%97%A0%E7%A9%B7%E6%95%B0%E7%BB%84%E8%A7%A3%E5%BA%8F%E5%88%97%E9%97%AE%E9%A2%98&quot; aria-label=&quot;用无穷数组解序列问题 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;用无穷数组解序列问题&lt;/h1&gt;&lt;p&gt;无穷数组对序列的描述能力很强。定义符号 ”++” 代表链接两个序列，那么&lt;/p&gt;&lt;p&gt;等差数列可以这样描述&lt;/p&gt;&lt;div class=&quot;math math-display&quot;&gt;f(a_0, b) = [a_0] ++ f(a_0 + b, b)&lt;/div&gt;&lt;p&gt;斐波那契数列的一般描述方式是&lt;/p&gt;&lt;div class=&quot;math math-display&quot;&gt;f(0) = 1 \newline
f(1) = 1
f(n) = f(n -1) + f(n-2)&lt;/div&gt;&lt;p&gt;但通过递归的无穷数组的方式我们还可以这样表达&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;span class=&quot;math math-inline&quot;&gt;f(a, b) = [a] ++ f(b, a +b)&lt;/span&gt;&lt;/li&gt;&lt;li&gt;斐波那契数列 = &lt;span class=&quot;math math-inline&quot;&gt;f(1, 1)&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;对于下面这道题也可以用递归的方法描述&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b0e88e1993b64700a83505ccbde3ba19~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;
&lt;a href=&quot;http://link.zhihu.com/?target=https%3A//www.codewars.com/kata/5672682212c8ecf83e000050&quot;&gt;原题链接&lt;/a&gt;&lt;/p&gt;&lt;p&gt;先卖个关子，看看普通的解决思路是什么样的。&lt;/p&gt;&lt;h2 id=&quot;一般解法-typescript&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E4%B8%80%E8%88%AC%E8%A7%A3%E6%B3%95-typescript&quot; aria-label=&quot;一般解法 typescript permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;一般解法 （TypeScript）&lt;/h2&gt;&lt;p&gt;&lt;img src=&quot;https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9be128220d7549d7b563816d266b7dc0~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;p&gt;通过链表实现。&lt;/p&gt;&lt;h2 id=&quot;haskell-解法&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#haskell-%E8%A7%A3%E6%B3%95&quot; aria-label=&quot;haskell 解法 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Haskell 解法&lt;/h2&gt;&lt;p&gt;&lt;img src=&quot;https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/886cd1df174443bfb1bd832fec0d813b~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;p&gt;看起来简单很多有没有！&lt;/p&gt;&lt;h3 id=&quot;理解-haskell-解法&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E7%90%86%E8%A7%A3-haskell-%E8%A7%A3%E6%B3%95&quot; aria-label=&quot;理解 haskell 解法 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;理解 Haskell 解法&lt;/h3&gt;&lt;p&gt;为了方便理解，我们可以泛化序列 u 的定义到 f(n)&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1be575eb50994016adefba3cb0b773fc~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;p&gt;在这个定义下，我们需要的序列 u 就等于排序后的 f(1)。&lt;/p&gt;&lt;p&gt;可以注意到 f(n) 符合这样的性质：&lt;/p&gt;&lt;div class=&quot;math math-display&quot;&gt;f(n) = \{n\} \cup f(2n + 1) \cup f(3n+1)&lt;/div&gt;&lt;p&gt;Haskell 的解法可以理解为通过无穷数组去描述了这个递归的性质。&lt;/p&gt;&lt;p&gt;因为 Haskell 的语法比较与众不同，为了方便理解，下面是将它朴素地翻译成 JS 之后的样子&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/462acef835664c83b3cb92a86bbe7cde~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;p&gt;next 对应上文提到的 &lt;span class=&quot;math math-inline&quot;&gt;f(n)&lt;/span&gt;。next 中的递归是没有边界条件的，在没有惰性求值特性的 JS 当中就会死循环。而在 Haskell 中，一个数组被定义后其中的值是没有被求出来的，只有当其中的值被最外层的 IO 获取的时候才会去计算具体的值。&lt;/p&gt;&lt;p&gt;而在 JS 中我们是否有语言特性可以实现惰性求值呢？也有！那就是 Generator 。所以我们可以通过 Generator 构造一个类似的解&lt;/p&gt;&lt;h2 id=&quot;在-js-中通过-generator-模仿无穷数组&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%9C%A8-js-%E4%B8%AD%E9%80%9A%E8%BF%87-generator-%E6%A8%A1%E4%BB%BF%E6%97%A0%E7%A9%B7%E6%95%B0%E7%BB%84&quot; aria-label=&quot;在 js 中通过 generator 模仿无穷数组 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;在 JS 中通过 Generator 模仿无穷数组&lt;/h2&gt;&lt;p&gt;通过无穷数组我们就可以通过 f(n) 的性质 &lt;span class=&quot;math math-inline&quot;&gt;f(n) = \{n\} \cup f(2n + 1) \cup f(3n+1)&lt;/span&gt; 实现：&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2e5c34a0c96445d09115809448ab4a10~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;性能对比&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;通过 Generator 的方式在性能上一定会有所损耗，以下是 index = 1e6 时的耗时对比。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e56d91154ba84745bf7091e861a88760~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;p&gt;性能还是比 Haskell 好的！&lt;/p&gt;&lt;h1 id=&quot;在-javascript-中实现一个惰性数组&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#%E5%9C%A8-javascript-%E4%B8%AD%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E6%83%B0%E6%80%A7%E6%95%B0%E7%BB%84&quot; aria-label=&quot;在 javascript 中实现一个惰性数组 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;在 JavaScript 中实现一个惰性数组&lt;/h1&gt;&lt;h2 id=&quot;specification&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#specification&quot; aria-label=&quot;specification permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Specification&lt;/h2&gt;&lt;p&gt;刚刚的例子中我们仅通过 Generator 模仿了 Haskell 解法，那有没有可能在 JS 当中封装一个惰性数组呢？它需要符合以下规格&lt;/p&gt;&lt;ul&gt;&lt;li&gt;值的计算是 Lazy 的&lt;/li&gt;&lt;li&gt;已经被计算的值会被缓存&lt;/li&gt;&lt;li&gt;长度可以是无穷大的&lt;/li&gt;&lt;li&gt;数组内容不能被改变&lt;/li&gt;&lt;li&gt;可获取长度&lt;/li&gt;&lt;li&gt;可以连接几个惰性数组，产生一个新的惰性数组&lt;/li&gt;&lt;li&gt;可被遍历&lt;/li&gt;&lt;li&gt;可获取指定下标的元素&lt;/li&gt;&lt;li&gt;定义可以是递归的&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;从而我们可以这样使用它&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8be3b5345e2c41f4b261b1a23e7300a4~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5f8cc8ec81fa4853888ce4970f7ce5cc~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;h2 id=&quot;v0-递归-generator&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#v0-%E9%80%92%E5%BD%92-generator&quot; aria-label=&quot;v0 递归 generator permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;v0. 递归 Generator&lt;/h2&gt;&lt;p&gt;&lt;img src=&quot;https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/996d6cfb49824a179b8fae85d4db2a1e~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;p&gt;朴素的 LazyArray 实现（容易爆栈） LazyArray@v0&lt;/p&gt;&lt;p&gt;直接通过递归地调用 self generator 函数就可以完成~&lt;/p&gt;&lt;p&gt;但这个实现方式有一个问题：太容易爆栈了，例如下面的代码就会直接导致溢出。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8d7a48bb63d04da5b370c3cc6546dc1f~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;p&gt;因为在递归的方式中调用栈是这样的&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/19e4f5d168d44b0c940bba2cd71c9d9c~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;p&gt;而栈的深度有限，根据宿主环境不同在&lt;a href=&quot;http://link.zhihu.com/?target=https%3A//stackoverflow.com/questions/7826992/browser-javascript-stack-size-limit&quot;&gt;几千到几万不等&lt;/a&gt;。&lt;/p&gt;&lt;h2 id=&quot;v1-防爆栈优化存在-bug&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#v1-%E9%98%B2%E7%88%86%E6%A0%88%E4%BC%98%E5%8C%96%E5%AD%98%E5%9C%A8-bug&quot; aria-label=&quot;v1 防爆栈优化存在 bug permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;v1. 防爆栈优化（存在 Bug）&lt;/h2&gt;&lt;p&gt;那有什么方法可以解决这样的爆栈问题呢 —— 将递归转为迭代。&lt;/p&gt;&lt;p&gt;但 LazyArray 的定义是要支持递归的。有什么方法可以让一个递归的定义通过迭代的方式来表达呢？我们可以替换递归过程中使用的函数~&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2a96581939bd456390fafe4d252d1472~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;p&gt;此时我们的调用过程如下图所示，调用栈始终只有一层。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/692b2072798d4ef69cf41314fb9c04ac~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;p&gt;这下怎么都不会溢出了~&lt;/p&gt;&lt;p&gt;emmmmmm，但是这个版本&lt;strong&gt;是有 Bug 的&lt;/strong&gt;。因为这个优化假设了每个 self 函数中 yield 的值都是会被 yield 到最外层的。例如在进行了这一项优化后，下面的例子中的 map 就起不了作用了&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5f7aa704d7ae4ef2a74018e238f61e35~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;p&gt;因为我们将其中的 self(n) 的 Generator 屏蔽了，导致 map 拿到了空的序列。&lt;/p&gt;&lt;p&gt;所以在进行这项优化时我们还需要判断下一个 self 函数返回的 Generator 是不是直接被上一层的 self 函数的 Generator 消费的。只有这样我们才能进行优化。&lt;/p&gt;&lt;h2 id=&quot;v2-符合条件才进行转递归为迭代&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#v2-%E7%AC%A6%E5%90%88%E6%9D%A1%E4%BB%B6%E6%89%8D%E8%BF%9B%E8%A1%8C%E8%BD%AC%E9%80%92%E5%BD%92%E4%B8%BA%E8%BF%AD%E4%BB%A3&quot; aria-label=&quot;v2 符合条件才进行转递归为迭代 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;v2. 符合条件才进行转递归为迭代&lt;/h2&gt;&lt;p&gt;&lt;img src=&quot;https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4096a1ea6daf4673bfa90b8bd5495eb4~tplv-k3u1fbpfcp-watermark.image&quot; alt=&quot;image.png&quot;/&gt;&lt;/p&gt;&lt;p&gt;使用它重新解一遍 Twice Linear 也是可以的 （😅 写起来的复杂度没啥变化）&lt;/p&gt;&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; LazyArray &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;quot;https://raw.githubusercontent.com/zxch3n/lab/master/lazy-array/lazy-array.ts&amp;quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;merge&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Iterator&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Iterator&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; va &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; vb &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;va &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; vb&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; va&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      va &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;va &lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; vb&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; vb&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      vb &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; vb&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      va &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      vb &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; f &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; LazyArray&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;createFactory&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; self&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; LazyArray&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;n&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;merge&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; n &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; n &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ans &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;solve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; ans&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;LazyArray 的实现：&lt;a href=&quot;https://github.com/zxch3n/lab/tree/master/lazy-array&quot;&gt;zxch3n/lab&lt;/a&gt;&lt;/p&gt;&lt;p&gt;TwiceLinear 相关代码：&lt;a href=&quot;https://github.com/zxch3n/blog/tree/master/twice_linear&quot;&gt;zxch3n/blog&lt;/a&gt;&lt;/p&gt;</content:encoded></item></channel></rss>