在先前我们讨论了视图层的适配器设计,主要是全量的视图初始化渲染,包括生命周期同步、状态管理、渲染模式、DOM映射状态等。在这里我们需要处理变更的增量更新,这属于性能方面的考量,需要考虑如何实现不可变的状态对象,以此来实现Op操作以及最小化DOM变更。
行级不可变状态
在这里我们先不引入视图层的渲染问题,而是仅在Model层面上实现精细化的处理,具体来说就是实现不可变的状态对象,仅更新的节点才会被重新创建,其他节点则直接复用。由此想来此模块的实现颇为复杂,也并未引入immer等框架,而是直接处理的状态对象,因此先从简单的更新模式开始考虑。
回到最开始实现的State模块更新文档内容,我们是直接重建了所有的LineState以及LeafState对象,然后在React视图层的BlockModel中监听了OnContentChange事件,以此来将BlockState的更新应用到视图层。
这种方式简单直接,全量更新状态能够保证在React的状态更新,然而这种方式的问题在于性能。当文档内容非常大的时候,全量计算将会导致大量的状态重建,并且其本身的改变也会导致React的diff差异进而全量更新文档视图,这样的性能开销通常是不可接受的。
那么通常来说我们就需要基于变更来确定状态的更新,首先我们需要确定更新的粒度,例如以行为基准则未变更的时候就直接取原有的LineState。相当于尽可能复用Origin List然后生成Target List,这样的方式自然可以避免部分状态的重建,尽可能复用原本的对象。
整体思路大概是先执行变成生成最新的列表,然后分别设置旧列表和新列表的row和col两个指针值,然后更新时记录起始row,删除和新增自然是正常处理,对于更新则认为是先删后增。对于内容的处理则需要分别讨论单行和跨行的问题,中间部分的内容就作为重建的操作。
最后可以将这部分增删LineState数据放置于Changes中,就可以得到实际增删的Ops了,这样我们就可以优化部分的性能,因为仅原列表和目标列表的中间部分才会重建,其他部分的行状态直接复用。此外这部分数据在apply的delta中是不存在的,同样可以认为是数据的补充。
那么这里实际上是存在非常需要关注的点,我们现在维护的是状态模型,也就是说所有的更新就不再是直接的compose,而是操作我们实现的状态对象。本质上我们是需要实现行级别的compose方法,这里的实现非常重要,假如我们对于数据的处理存在偏差的话,那么就会导致状态出现问题。
此外在这种方式中,我们判断LineState是否需要新建则是根据整个行内的所有LeafState来重建的。也就是说这种时候我们是需要再次将所有的op遍历一遍,当然实际上由于最后还需要将compose后的Delta切割为行级别的内容,所以其实即使在应用变更后也最少需要再遍历两次。
那么此时我们需要思考优化方向,首先是首个retain,在这里我们应该直接完整复用原本的LineState,包括处理后的剩余节点也是如此。而对于中间的节点,我们就需要为其独立设计更新策略,这部分理论上来说是需要完全独立处理为新的状态对象的,这样可以减少部分Leaf Op的遍历。
其中,如果是新建的节点,我们直接构建新的LineState即可,删除的节点则不从原本的LineState中放置于新的列表。而对于更新的节点,我们需要更新原本的LineState对象,因为实际上行是存在更新的,而重点是我们需要将原本的LineState的key值复用。
这里我们先简单实现实现描述一下复用的问题,比较方便的实现则是直接以\n的标识为目标的State,这就意味着我们要独立\n为独立的状态。即如果在123|456\n的|位置插入\n的话,那么我们就是123是新的LineState,456是原本的LineState,以此来实现key的复用。
其实这里有个非常值得关注的点是,LineState在Delta中是没有具体对应的Op的,而相对应的LeafState则是有具体的Op的。这就意味着我们在处理LineState的更新时,是不能直接根据变更控制的,因此必须要找到能够映射的状态,因此最简单的方案即根据\n节点映射。
patek-shs.sxjshdzb.com
patek-bjs.sxjshdzb.com
patek-hzs.sxjshdzb.com
patek-shs.xajshdzb.com
patek-hzs.xajshdzb.com
patek-cds.xajshdzb.com
patek-gebs.xajshdzb.com
patek-tjs.xajshdzb.com
patek-shs.sxjshd.com
patek-gebs.sxjshd.com
patek-shs.watch51.com
patek-bjs.watch51.com
patek-hzs.watch51.com
patek-cds.watch51.com
patek-gebs.watch51.com
patek-hzs.jshdcq.com
patek-gebs.jshdcq.com
patek-tjs.jshdcq.com
patek-shs.richardweixiu.com
patek-bjs.richardweixiu.com
patek-hzs.richardweixiu.com
patek-shs.watchjt.com
patek-hzs.watchjt.com
patek-cds.watchjt.com
patek-gebs.watchjt.com
patek-jns.watchjt.com
patek-shs.jshdzg.com
patek-wxs.jshdzg.com
patek-shs.jshdsx.com
patek-bjs.jshdsx.com
patek-hzs.jshdsx.com
patek-cds.jshdsx.com
patek-wxs.jshdsx.com
patek-hzs.guoshew.com
patek-wxs.guoshew.com
patek-jns.guoshew.com
patek-shs.ncjshdzb.com
patek-bjs.ncjshdzb.com
patek-hzs.ncjshdzb.com
patek-hzs.jsddshwx.com
patek-wxs.jsddshwx.com
patek-ccs.jsddshwx.com
patek-dgs.jsddshwx.com
patek-ncs.jsddshwx.com
patek-hzs.hnjshdzb.com
patek-tys.hnjshdzb.com
patek-hzs.hljjshd.com
patek-cds.hljjshd.com
patek-wxs.hljjshd.com
patek-ccs.hljjshd.com
实际上我们可以总结一下,最开始我们考虑先更新再diff,后来考虑的是边更新边记录。边更新边记录的优点在于,可以避免再次遍历一边所有Leaf节点的消耗,同时也可以避免diff的复杂性。但是这里也存在个问题,如果内部进行了多次retain操作,则无法直接复用LineState。
不过通常来说,最高频的操作是输入内容,这种情况下首操作一般都是retain,尾操作为空会收集剩余文档内容,因此这部分优化是会被高频触发的。而如果是多次的内容部分变更操作,这部分虽然可以通过判断行内的叶子结点是否变更,来判断是否复用行对象,但是也存在一定复杂性。
关于这部分的具体实现,在编辑器的状态模块里存在独立的Mutate模块,这部分实现在后边实现各个模块时会独立介绍。到这里我们就可以实现一个简单的Immutable状态维护,如果Leaf节点发生变化之后,其父节点Line会触发更新,而其他节点则可以直接复用。