OperationalTransformation(OT)是应用在协同编辑领域的并发控制和冲突解决系统,解决“多个用户编辑同一文本域的同一版本”的处理方法与它”的问题。以下简称冲突问题。总体而言,OT解决并发编辑冲突的方法有以下几个步骤:定义原子操作类型:将用户在UI上触发的基于事件的操作抽象为一个由可枚举的N个原子操作类型组成的操作序列,这样,将复杂UI界面操作的冲突转化为原子操作之间的冲突,同时定义了多端数据同步的基本数据结构。设计冲突解决算法:当出现冲突问题时,通过冲突解决算法解决冲突。冲突解决算法应完成以下任务:算法将两个冲突的操作序列转换为两个新的可以与原冲突操作序列串行执行的操作序列。Serializable两组操作序列作用于文本,效果相同。序列化后的操作需要保持原有的语义。多端冲突解决:通过向每一端同步一个新的串行操作序列来完成冲突解决。接下来我将通过ot.js的实现来详细讲解冲突解决的思路。定义原子操作类型。我们先来看一个没有原子操作的世界。对于有状态的UIComponent,其实是没有操作类型的概念的。每次UI操作只会产生newValue,在浏览器端通过e.target.value获取。如果进行粗略的oldValue和newValuediff,在浏览器文本框中,用户可以进行如下UI操作:headinsertion:"a"=>"ba"tailinsertion:"a"=>"ab"middleinsertion:"ab"=>"acb"删除和插入:"abbbbb"=>"ac"(选择bbbbb,输入c)删除头部,删除尾部,删除中间,删除多个字符。可以看到用户可以删除UI上的操作类型太多,缺乏定义,与UI组件绑定没有普适性。所以我们需要定义一些更加简洁和原子化的操作。ot.js中定义的操作是:retain(Integern):保留n个字符,可以看成是将字符串数组的指针移动到下标为n的位置insert(Stringstr):在当前位置插入strdelete(Stringstr):删除当前位置后面的str,定义原子操作后,UI操作可以表示为由原子操作组成的操作序列://headinsert:"a"=>"ba"insert("b");//在末尾插入:"a"=>"ab"retain(1).insert("b")//在中间插入:`"ab"=>"acb"`retain(1).insert("c")//删除并插入:`"abbbbb"=>"ac"`(选择bbbbb,输入c)retain(1).delete("bbbbb").insert("c")调用后上面的原子操作,ot.js中的操作顺序会在一个叫做TextOperation的类中维护//删除和插入:`"abbbbb"=>"ac"`(selectbbbbb,enterc)TextOperationto=newTextOperation().retain(1).delete("bbbbb").insert("c");//to.ops=[Retain(1),Insert("bbb"),Delete("c")]andTextOperation是multi-终端同步同时也实现了操作序列化,理解冲突问题。既然算法要解决冲突问题,那我们先回顾一下什么是冲突问题:冲突问题是:“多个用户编辑同一个文本域的同一个版本”Howtodeal先处理“如何处理”的问题在这里解释一下关键词。正确理解冲突问题非常重要:1、同文本域:对于不同的底层数据模型,“同文本域”的概念是不同的。对于<文本区域>,一个文本域的概念必须是这个文本框中的所有文本,但是像Notion、RoamResearch或飞书这样的应用,它们的每一行都是一个块,每个块都可以看作一个单独的Textarea此时“同一个文本域”的概念可能是文档中的每一个块,原子操作类型的设计会更加复杂(例如:块插入、移动、删除,当块嵌入组件时,组件内部如何协作),具体定义取决于应用程序的设计。但无论如何,只有对同一个文本域的修改才是我们这里所说的冲突。2.Version:使用全球唯一且自增的数字标识,在协同编辑领域称为revision。生成新的TextOperation时,当前Client的revision加1。版本号一方面表示文本的状态,另一方面也决定了操作是否可以应用到当前文本。(PS:文本字段的版本之所以没有使用字符串比较,避免了ABA问题应该也是决定因素之一)。设计一个冲突解决算法就已经明白什么是冲突问题了,所以从ot.js的角度来看,有两个TextOperations作用在同一个revision的文本字段上,每个TextOperation携带一组原子操作类型序列。那么我们应该如何处理这两个TextOperations才能让冲突自动解决呢?解决冲突的方法是:当原子操作作用于文本中相同的位置时,我们通过增加一个新的原子操作或者拆分当前的原子操作使其可序列化。自动解决方案是:将定义好的可枚举的原子操作类型retain();insert();delete()排列组合,形成9种组合。相当于枚举了所有的冲突情况,针对每种情况设计了解决方案后,算法可以在冲突发生时自动解决冲突。下面用具体的例子来说明添加操作的冲突解决方法。两个操作都操作在0的位置。OperationA和operationB通过冲突解决算法序列化。operationA中的insert操作先执行,operationB通过添加一个Retain()操作,从而保留了A操作的效果。生成了两个新操作。在新的操作中,newOperationA和之前一样,newOperationB多了一个院子操作。//Text:""//operationA:""=>"a"operationA.ops=[Insert("a")];//operationB:""=>"b"operationB.ops=[Insert("b")];//冲突解决transform(operationA,operationB)//{newOperationA:[Insert("a")],newOperationB:[Retain(1),Insert("b")]}如果我们只针对这个写本例中transform方法的处理逻辑,代码可能如下所示newOperationB=newTextOperation();//operationA和operationB的原子操作序列opsA=operationA.ops;opsB=operationB.ops;//遍历opA和opB的指针indexA=0;索引B=0;while(indexA
