Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

协同编辑 - OT算法 #28

Open
z2014 opened this issue Dec 27, 2020 · 1 comment
Open

协同编辑 - OT算法 #28

z2014 opened this issue Dec 27, 2020 · 1 comment

Comments

@z2014
Copy link
Owner

z2014 commented Dec 27, 2020

对于在线文档的难点,大部分同学的第一反应都是协同编辑,如何解决多人协作的冲突的问题。

关于ot协作的介绍,这篇文章已经有了一定的初步介绍,本文在这篇文章之上,精读一下ot.js这个库,一起来学习下如何实现一个ot.js。

ot.js中核心的文件是text-operation.js文件,本文精读也将围绕它展开。

对于协同编辑场景,都要解决哪些问题呢?

  1. 支持将多次操作合并成一次
  2. 对不同用户的多次操作进行合并,并返回相对应的opts,使不同用户的界面展示保持一致。
  3. 对于用户的操作支持回退

要实现上面这3个需求,我们先来看看如何设计ot算法中的数据结构。

  function TextOperation () {
    if (!this || this.constructor !== TextOperation) {
      // => function was called without 'new'
      return new TextOperation();
    }

    // When an operation is applied to an input string, you can think of this as
    // if an imaginary cursor runs over the entire string and skips over some
    // parts, deletes some parts and inserts characters at some positions. These
    // actions (skip/delete/insert) are stored as an array in the "ops" property.
    this.ops = [];
    // An operation's baseLength is the length of every string the operation
    // can be applied to.
    this.baseLength = 0;
    // The targetLength is the length of every string that results from applying
    // the operation on a valid input string.
    this.targetLength = 0;
  }

可以看到我们对于字符串的操作都保存在了ops数组里,
一共会有3种类型的操作,分别是保留,删除,和插入.
数据结构中还记录了字符串初始化的长度和操作之后的目标长度,
用来做一些opts的判断(eg:equals方法)。

1. 基础方法apply

  TextOperation.apply = function (operation, str) {
    // var operation = this;
    if (str.length !== operation.baseLength) {
      throw new Error("The operation's base length must be equal to the string's length.");
    }
    var newStr = [], j = 0;
    var strIndex = 0;
    var ops = operation.ops;
    for (var i = 0, l = ops.length; i < l; i++) {
      var op = ops[i];
      if (isRetain(op)) {
        if (strIndex + op > str.length) {
          throw new Error("Operation can't retain more characters than are left in the string.");
        }
        // Copy skipped part of the old string.
        newStr[j++] = str.slice(strIndex, strIndex + op);
        strIndex += op;
      } else if (isInsert(op)) {
        // Insert string.
        newStr[j++] = op;
      } else { // delete op
        strIndex -= op;
      }
      console.log(newStr, strIndex)
    }
    if (strIndex !== str.length) {
      throw new Error("The operation didn't operate on the whole string.");
    }
    return newStr.join('');
  }

tips:因为delete操作,存的值是负数,所以这里是 strIndex -= op。

2. 基础方法invert

  TextOperation.prototype.invert = function (str) {
    var strIndex = 0;
    var inverse = new TextOperation();
    var ops = this.ops;
    for (var i = 0, l = ops.length; i < l; i++) {
      var op = ops[i];
      if (isRetain(op)) {
        inverse.retain(op);
        strIndex += op;
      } else if (isInsert(op)) {
        inverse['delete'](op.length);
      } else { // delete op
        inverse.insert(str.slice(strIndex, strIndex - op));
        strIndex -= op;
      }
    }
    return inverse;
  };

我们针对原有opts数组生成保存回退操作的相对应的opts。
image

3. 核心方法compose解析

这个方法是用来将两个opts数组合并成一个,
也就是s.apply(a, b) = s.apply(compose(a, b));
因为源代码比较长,这里我们就只解析下compose的思路。
要将两个opts合并,那么我们就维护两个指针,移动对应的两个opts数组:
(以下简写a=opts1[i]; b = opts2[j])
在合并opts的过程中,我们始终要保证a和b是对当前字符串同一位置所进行的操作。

image

  • 1). a的删除操作是第一优先级,因为b的操作(保留,删除,添加)是基于a的操作之后进行的动作,因此需要先执行a的删除操作。
  • 2). b的插入操作是第二优先级,在相同位置下,b的添加操作,从结果上看,都是先于a的保留或者添加的。
  • 3). 如果a,b都是保留操作,那我们就保留两个中的公共长度
  • 4). 如果a是插入,b是删除,那么我们就将两个操作的相同长度进行合并,保留操作长度剩下的部分,继续遍历
  • 5). 如果a是插入,b是保留,那么我们就插入两个操作的公共长度,保留操作长度更长的部分,继续遍历
  • 6). 如果a是保留,b是删除,那么我们就删除两个操作的公共长度,保留操作长度更长的剩余部分,继续遍历

4. 核心方法transform解析

和compose方法类似,只是transform是为了将两个opts生成相对应互补的opts,来解决保证两个用户的界面展示一致,解决问题二。也就是 `s.apply(compose(a, b')) = s.apply(compose(b, a')).

image

  • 1). 如果a是插入操作,那么相对应的a'也应该插入相同的元素, b'就应该保留a'的长度(b操作为插入时一样)。
  • 2). 如果a和b都是保留操作,那么a'和b'都应该保留min(a, b)的长度
  • 3). 如果a和b都是删除操作,那么我们只需要保留两边删除元素不一致的地方即可,然后进入下面的判断
  • 4). 如果a是删除,b是保留,那么a'需要添加min(a, b)的长度,进入下一轮遍历
  • 5). 如果a是保留,b是删除,和操作5一样。
@Jicoh
Copy link

Jicoh commented Mar 28, 2022

VG

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants