| 查看: 138 | 回复: 0 | |||
[交流]
从零实现富文本编辑器#11-Immutable状态维护与增量渲染
|
|
在这里我们先不引入视图层的渲染问题,而是仅在Model层面上实现精细化的处理,具体来说就是实现不可变的状态对象,仅更新的节点才会被重新创建,其他节点则直接复用。由此想来此模块的实现颇为复杂,也并未引入immer等框架,而是直接处理的状态对象,因此先从简单的更新模式开始考虑。 回到最开始实现的State模块更新文档内容,我们是直接重建了所有的LineState以及LeafState对象,然后在React视图层的BlockModel中监听了OnContentChange事件,以此来将BlockState的更新应用到视图层。 Copy delta.eachLine((line, attributes, index) => { const lineState = new LineState(line, attributes, this); lineState.index = index; lineState.start = offset; lineState.key = Key.getId(lineState); offset = offset + lineState.length; this.lines[index] = lineState; }); 这种方式简单直接,全量更新状态能够保证在React的状态更新,然而这种方式的问题在于性能。当文档内容非常大的时候,全量计算将会导致大量的状态重建,并且其本身的改变也会导致React的diff差异进而全量更新文档视图,这样的性能开销通常是不可接受的。 那么通常来说我们就需要基于变更来确定状态的更新,首先我们需要确定更新的粒度,例如以行为基准则未变更的时候就直接取原有的LineState。相当于尽可能复用Origin List然后生成Target List,这样的方式自然可以避免部分状态的重建,尽可能复用原本的对象。 整体思路大概是先执行变成生成最新的列表,然后分别设置旧列表和新列表的row和col两个指针值,然后更新时记录起始row,删除和新增自然是正常处理,对于更新则认为是先删后增。对于内容的处理则需要分别讨论单行和跨行的问题,中间部分的内容就作为重建的操作。 最后可以将这部分增删LineState数据放置于Changes中,就可以得到实际增删的Ops了,这样我们就可以优化部分的性能,因为仅原列表和目标列表的中间部分才会重建,其他部分的行状态直接复用。此外这部分数据在apply的delta中是不存在的,同样可以认为是数据的补充。 Copy Origin List (Old) Target List (New) +-------------------+ +-------------------+ | [0] LineState A | <---- Retain ------> | [0] LineState A | (Reused) +-------------------+ +-------------------+ | [1] LineState B | | | [1] LineState B2 | (Update) +-------------------+ Changes | (Modified) | (Del C) | [2] LineState C | | +-------------------+ +-------------------+ V | [2] NewState X | (Inserted) | [3] LineState D | ---------------\ +-------------------+ +-------------------+ \--> | [3] LineState D | (Reused) | [4] LineState E | <---- Retain ------> | [4] LineState E | (Reused) +-------------------+ +-------------------+ 那么这里实际上是存在非常需要关注的点,我们现在维护的是状态模型,也就是说所有的更新就不再是直接的compose,而是操作我们实现的状态对象。本质上我们是需要实现行级别的compose方法,这里的实现非常重要,假如我们对于数据的处理存在偏差的话,那么就会导致状态出现问题。 此外在这种方式中,我们判断LineState是否需要新建则是根据整个行内的所有LeafState来重建的。也就是说这种时候我们是需要再次将所有的op遍历一遍,当然实际上由于最后还需要将compose后的Delta切割为行级别的内容,所以其实即使在应用变更后也最少需要再遍历两次。 那么此时我们需要思考优化方向,首先是首个retain,在这里我们应该直接完整复用原本的LineState,包括处理后的剩余节点也是如此。而对于中间的节点,我们就需要为其独立设计更新策略,这部分理论上来说是需要完全独立处理为新的状态对象的,这样可以减少部分Leaf Op的遍历。 Copy new Delta().retain(5).insert("xx" ![]() insert("123" , insert("\n" // skip insert("456" , insert("\n" // new line state其中,如果是新建的节点,我们直接构建新的LineState即可,删除的节点则不从原本的LineState中放置于新的列表。而对于更新的节点,我们需要更新原本的LineState对象,因为实际上行是存在更新的,而重点是我们需要将原本的LineState的key值复用。 这里我们先简单实现实现描述一下复用的问题,比较方便的实现则是直接以\n的标识为目标的State,这就意味着我们要独立\n为独立的状态。即如果在123|456\n的|位置插入\n的话,那么我们就是123是新的LineState,456是原本的LineState,以此来实现key的复用。 Copy [ insert("123" , insert("\n" , insert("456" , insert("\n"![]() ] // ===> [ LineState(LeafState("123" , LeafState("\n" ), LineState(LeafState("456" , LeafState("\n" )] 其实这里有个非常值得关注的点是,LineState在Delta中是没有具体对应的Op的,而相对应的LeafState则是有具体的Op的。这就意味着我们在处理LineState的更新时,是不能直接根据变更控制的,因此必须要找到能够映射的状态,因此最简单的方案即根据\n节点映射。 Copy LeafState("\n", key="1" <=> LineState(key="L1"![]() 实际上我们可以总结一下,最开始我们考虑先更新再diff,后来考虑的是边更新边记录。边更新边记录的优点在于,可以避免再次遍历一边所有Leaf节点的消耗,同时也可以避免diff的复杂性。但是这里也存在个问题,如果内部进行了多次retain操作,则无法直接复用LineState。 不过通常来说,最高频的操作是输入内容,这种情况下首操作一般都是retain,尾操作为空会收集剩余文档内容,因此这部分优化是会被高频触发的。而如果是多次的内容部分变更操作,这部分虽然可以通过判断行内的叶子结点是否变更,来判断是否复用行对象,但是也存在一定复杂性。 关于这部分的具体实现,在编辑器的状态模块里存在独立的Mutate模块,这部分实现在后边实现各个模块时会独立介绍。到这里我们就可以实现一个简单的Immutable状态维护,如果Leaf节点发生变化之后,其父节点Line会触发更新,而其他节点则可以直接复用。 Key 值维护# 至此我们实现了一套简单的Immutable Delta+Iterator来处理更新,这种时候我们就可以借助不可变的方式来实现React视图的更新,那么在React的渲染模式中,key值的管理也是个值的探讨的问题。 在这里我们就可以根据状态不可变来生成key值,借助WeakMap映射关系获取对应的字符串id值,此时就可以借助key的管理以及React.memo来实现视图的复用。其实在这里初步看起来key值应该是需要主动控制强制刷新的时候,以及完全是新节点才会用得到的。 但是这种方式也是有问题的,因为此时我们即使输入简单的内容,也会导致整个行的key发生改变,而此时我们是不必要更新此时的key的。因此key值是需要单独维护的,不能直接使用不可变的对象来索引key值,那么如果是直接使用index作为key值的话,就会存在潜在的原地复用问题。 key值原地复用会导致组件的状态被错误保留,例如此时有个非受控管理的input组件列表,在某个输入框内已经输入了内容,当其发生顺序变化时,原始输入内容会跟随着原地复用的策略留在原始的位置,而不是跟随到新的位置,因为其整体列表顺序key未发生变化导致React直接复用节点。 在LineState节点的key值维护中,如果是初始值则是根据state引用自增的值,在变更的时候则是尽可能地复用原始行的key,这样可以避免过多的行节点重建并且可以控制整行的强制刷新。 而对于LeafState节点的key值最开始是直接使用index值,这样实际上会存在隐性的问题,而如果直接根据Immutable来生成key值的话,任何文本内容的更改都会导致key值改变进而导致DOM节点的频繁重建。 Copy export const NODE_TO_KEY = new WeakMap<Object.Any, Key>(); export class Key { /** 当前节点 id */ public id: string; /** 自动递增标识符 */ public static n = 0; constructor() { this.id = `${Key.n++}`; } /** * 根据节点获取 id * @param node */ public static getId(node: Object.Any): string { let key = NODE_TO_KEY.get(node); if (!key) { key = new Key(); NODE_TO_KEY.set(node, key); } return key.id; } } 通常使用index作为key是可行的,然而在一些非受控场景下则会由于原地复用造成渲染问题,diff算法导致的性能问题我们暂时先不考虑。在下面的例子中我们可以看出,每次我们都是从数组顶部删除元素,而实际的input值效果表现出来则是删除了尾部的元素,这就是原地复用的问题。在非受控场景下比较明显,而我们的ContentEditable组件就是一个非受控场景,因此这里的key值需要再考虑一下。 Copy const { useState, Fragment, useRef, useEffect } = React; function App() { const ref = useRef<HTMLParagraphElement>(null); const [nodes, setNodes] = useState(() => Array.from({ length: 10 }, (_, i) => i)); const onClick = () => { const [_, ...rest] = nodes; console.log(rest); setNodes(rest); }; useEffect(() => { const el = ref.current; el && Array.from(el.children).forEach((it, i) => ((it as HTMLInputElement).value = i + "" );}, []); return ( <Fragment> <p ref={ref}> {nodes.map((_, i) => (<input key={i}></input> )}</p> <button onClick={onClick}>slice</button> </Fragment> ); } 考虑到先前提到的我们不希望任何文本内容的更改都导致key值改变引发重建,因此就不能直接使用计算的immutable对象引用来处理key值,而描述单个op的方法除了insert就只剩下attributes了。 但是如果基于attributes来获得就需要精准控制合并insert的时候取需要取旧的对象引用,且没有属性的op就不好处理了,因此这里可能只能将其转为字符串处理,但是这样同样不能保持key的完全稳定,因此前值的索引改变就会导致后续的值出现变更。 Copy const prefix = new WeakMap<LineState, Record<string, number>>(); const suffix = new WeakMap<LineState, Record<string, number>>(); const mapToString = (map: Record<string, string> : string => {return Object.keys(map) .map(key => `${key}:${map[key]}`) .join("," ;}; const toKey = (state: LineState, op: Op): string => { const key = op.attributes ? mapToString(op.attributes) : ""; const prefixMap = prefix.get(state) || {}; prefix.set(state, prefixMap); const suffixMap = suffix.get(state) || {}; suffix.set(state, suffixMap); const prefixKey = prefixMap[key] ? prefixMap[key] + 1 : 0; const suffixKey = suffixMap[key] ? suffixMap[key] + 1 : 0; prefixMap[key] = prefixKey; suffixMap[key] = suffixKey; return `${prefixKey}-${suffixKey}`; }; 在slate中我先前认为生成的key跟节点是完全一一对应的关系,例如当A节点变化时,其代表的层级key必然会发生变化。然而在关注这个问题之后,我发现其在更新生成新的Node之后,会同步更新Path以及PathRef对应的Node节点所对应的key值。 Copy for (const [pathRef, key] of pathRefMatches) { if (pathRef.current) { const [node] = Editor.node(e, pathRef.current) NODE_TO_KEY.set(node, key) } pathRef.unref() } 在后续观察Lexical实现的选区模型时,发现其是用key值唯一地标识每个叶子结点的,选区也是基于key值来描述的。整体表达上比较类似于Slate的选区结构,或者说是DOM树的结构。这里仅仅是值得Range选区,Lexical实际上还有其他三种选区类型。 Copy { anchor: { key: "51", offset: 2, type: "text" }, focus: { key: "51", offset: 3, type: "text" } } 在这里比较重要的是key值变更时的状态保持,因为编辑器的内容实际上是需要编辑的。然而如果做到immutable话,很明显直接根据状态对象的引用来映射key会导致整个编辑器DOM无效的重建。例如调整标题的等级,就由于整个行key的变化导致整行重建。 那么如何尽可能地复用key值就成了需要研究的问题,我们的编辑器行级别的key是被特殊维护的,即实现了immutable以及key值复用。而目前叶子状态的key依赖了index值,因此如果调研Lexical的实现,同样可以将其应用到我们的key值维护中。 通过在playground中调试可以发现,即使我们不能得知其是否为immutable的实现,依然可以发现Lexical的key是以一种偏左的方式维护。因此在我们的编辑器实现中,也可以借助同样的方式,合并直接以左值为准复用,拆分时若以0起始直接复用,起始非0则创建新key。 [123456(key1)][789(bold-key2)]文本,将789的加粗取消,整段文本的key值保持为key1。 [123456789(key1)]]文本,将789这段文本加粗,左侧123456文本的key值保持为key1,789则是新的key。 [123456789(key1)]]文本,将123这段文本加粗,左侧123文本的key值保持为key1,456789则是新的key。 [123456789(key1)]]文本,将456这段文本加粗,左侧123文本的key值保持为key1,456和789分别是新的key。 因此,此时在编辑器中我们也是用类似偏左的方式维护key,由于我们需要保持immutable,所以这里的表达实际上是尽可能复用先前的key状态。这里与LineState的key值维护方式类似,都是先创建状态然后更新其key值,当然还有很多细节的地方需要处理。 Copy // 起始与裁剪位置等同 NextOp => Immutable 原地复用 State https://vk.com/topic-237947067_60116855 https://vk.com/topic-237947067_60116986 https://vk.com/topic-237947067_60117032 https://vk.com/topic-237947067_60117094 https://vk.com/topic-237947067_60117130 https://vk.com/topic-237947067_60117167 https://vk.com/topic-237947067_60117215 https://vk.com/topic-237947067_60117268 https://vk.com/topic-237947067_60117325 https://vk.com/topic-237947067_60117376 https://vk.com/topic-237947067_60117427 https://vk.com/topic-237947067_60117475 https://vk.com/topic-237947067_60117526 https://vk.com/topic-237947067_60117591 https://vk.com/topic-237947067_60117636 https://vk.com/topic-237947067_60117713 https://vk.com/topic-237947067_60117762 https://vk.com/topic-237947067_60117807 https://vk.com/topic-237947067_60117847 https://vk.com/topic-237947067_60117890 https://vk.com/topic-237947067_60117933 https://vk.com/topic-237947067_60118039 https://vk.com/topic-237947067_60118092 https://vk.com/topic-237947067_60118149 https://vk.com/topic-237947067_60118212 https://vk.com/topic-237947067_60118276 https://vk.com/topic-237947067_60118336 https://vk.com/topic-237947067_60118391 https://vk.com/topic-237947067_60118440 https://vk.com/topic-237947067_60118482 https://vk.com/topic-237947067_60118536 https://vk.com/topic-237947067_60118589 https://vk.com/topic-237947067_60118634 https://vk.com/topic-237947067_60118682 https://vk.com/topic-237947067_60118744 https://vk.com/topic-237947067_60118808 https://vk.com/topic-237947067_60118861 https://vk.com/topic-237947067_60118913 https://vk.com/topic-237947067_60118965 https://vk.com/topic-237947067_60119013 https://vk.com/topic-237947067_60119062 https://vk.com/topic-237947067_60119105 https://vk.com/topic-237947067_60119148 https://vk.com/topic-237947067_60119202 https://vk.com/topic-237947067_60119244 https://vk.com/topic-237947067_60119286 https://vk.com/topic-237947067_60119327 https://vk.com/topic-237947067_60119375 https://vk.com/topic-237947067_60119425 https://vk.com/topic-237947067_60119494 https://vk.com/topic-237947067_60119661 https://vk.com/topic-237947067_60119707 https://vk.com/topic-237947067_60119755 https://vk.com/topic-237947067_60119805 https://vk.com/topic-237947067_60119848 https://vk.com/topic-237947067_60119889 https://vk.com/topic-237947067_60119924 https://vk.com/topic-237947067_60120067 https://vk.com/topic-237947067_60120115 https://vk.com/topic-237947067_60120160 https://vk.com/topic-237947067_60120209 https://vk.com/topic-237947067_60120243 https://vk.com/topic-237947067_60120318 https://vk.com/topic-237947067_60120368 https://vk.com/topic-237947067_60120404 https://vk.com/topic-237947067_60120443 https://vk.com/topic-237947067_60120494 https://vk.com/topic-237947067_60120531 https://vk.com/topic-237947067_60120567 https://vk.com/topic-237947067_60120613 https://vk.com/topic-237947067_60120655 https://vk.com/topic-237947067_60120699 https://vk.com/topic-237947067_60120740 https://vk.com/topic-237947067_60120782 https://vk.com/topic-237947067_60120822 https://vk.com/topic-237947067_60120863 https://vk.com/topic-237947067_60120915 https://vk.com/topic-237947067_60120975 https://vk.com/topic-237947067_60121016 https://vk.com/topic-237947067_60121060 https://vk.com/topic-237947067_60121105 https://vk.com/topic-237947067_60121159 https://vk.com/topic-237947067_60121189 https://vk.com/topic-237947067_60121231 https://vk.com/topic-237947067_60121274 https://vk.com/topic-237947067_60121330 https://vk.com/topic-237947067_60121388 https://vk.com/topic-237947067_60121457 https://vk.com/topic-237947067_60121543 https://vk.com/topic-237947067_60121621 https://vk.com/topic-237947067_60121693 https://vk.com/topic-237947067_60121732 https://vk.com/topic-237947067_60121777 https://vk.com/topic-237947067_60121839 https://vk.com/topic-237947067_60121870 https://vk.com/topic-237947067_60121900 https://vk.com/topic-237947067_60121947 https://vk.com/topic-237947067_60121987 https://vk.com/topic-237947067_60122022 https://vk.com/topic-237947067_60122055 https://vk.com/topic-237947067_60122099 https://vk.com/topic-237947067_60122143 https://vk.com/topic-237947067_60122182 https://vk.com/topic-237947067_60122215 https://vk.com/topic-237947067_60122249 https://vk.com/topic-237947067_60122279 https://vk.com/topic-237947067_60122315 https://vk.com/topic-237947067_60122354 https://vk.com/topic-237947067_60122388 https://vk.com/topic-237947067_60122408 https://vk.com/topic-237947067_60122431 https://vk.com/topic-237947067_60122462 https://vk.com/topic-237947067_60122495 https://vk.com/topic-237947067_60122529 https://vk.com/topic-237947067_60122562 https://vk.com/topic-237947067_60122602 https://vk.com/topic-237947067_60122645 https://vk.com/topic-237947067_60122713 https://vk.com/topic-237947067_60122768 https://vk.com/topic-237947067_60122815 https://vk.com/topic-237947067_60122839 https://vk.com/topic-237947067_60122863 https://vk.com/topic-237947067_60122892 https://vk.com/topic-237947067_60122918 https://vk.com/topic-237947067_60122956 https://vk.com/topic-237947067_60123010 https://vk.com/topic-237947067_60123070 https://vk.com/topic-237947067_60123110 https://vk.com/topic-237947067_60123199 https://vk.com/topic-237947067_60123232 https://vk.com/topic-237947067_60123267 https://vk.com/topic-237947067_60123302 https://vk.com/topic-237947067_60123341 https://vk.com/topic-237947067_60123379 https://vk.com/topic-237947067_60123413 https://vk.com/topic-237947067_60123469 |
» 猜你喜欢
研究生做的很差,你们会让毕业吗?
已经有11人回复
申博自荐
已经有8人回复
26年博士申请自荐-电催化
已经有8人回复
求碳排放博导;方向是LCA、生命周期可持续发展以及碳排放
已经有7人回复
2026博士申请求助
已经有4人回复
2026博士或科研助理转27年博士
已经有7人回复
急招2026年9月份入学博士
已经有3人回复
2026年博士申请求捞
已经有3人回复
国自科送审了吗
已经有11人回复
博士招生
已经有5人回复













回复此楼