接口定义

能够对于文字、段落乃至任何元素的精准定位 并做出增删改查,都是在开发一款富文本编辑器时一项最基本也是最重要的功能之一。让我们先来看看Slate中对于如何在文档树中定位元素是怎么定义的[源码]

  1. /**
  2. * The `Location` interface is a union of the ways to refer to a specific
  3. * location in a Slate document: paths, points or ranges.
  4. *
  5. * Methods will often accept a `Location` instead of requiring only a `Path`,
  6. * `Point` or `Range`. This eliminates the need for developers to manage
  7. * converting between the different interfaces in their own code base.
  8. */
  9. export type Location = Path | Point | Range

Location是一个包含了PathPointRange的联合类型,代指了Slate中所有关于“定位”的定位,同时也方便了开发。例如在几乎所有的Transforms方法中,都可以通过传递Location参数来决定Transforms方法需要应用到文档中的哪些位置上[链接]

All transforms support a parameter options. This includes options specific to the transform, and general NodeOptions to specify which Nodes in the document that the transform is applied to.

  1. interface NodeOptions {
  2. at?: Location
  3. match?: (node: Node, path: Location) => boolean
  4. mode?: 'highest' | 'lowest'
  5. voids?: boolean
  6. }

Path

Path是三个中最基本的概念,也是唯一一个不可拓展的类型。

  1. /**
  2. * `Path` arrays are a list of indexes that describe a node's exact position in
  3. * a Slate node tree. Although they are usually relative to the root `Editor`
  4. * object, they can be relative to any `Node` object.
  5. */
  6. export type Path = number[]

Path类型就是一个数组,用来表示Slate文档树中自根节点root到指定node的绝对路径。我们以下边的示例来演示下各个node所代表的路径:

  1. const initialValue: Descendant[] = [
  2. // path: [0]
  3. {
  4. type: 'paragraph',
  5. children: [
  6. { text: 'This is editable ' }, // path: [0, 0]
  7. { text: 'rich text!', bold: true } // path: [0, 1]
  8. ]
  9. },
  10. // path: [1]
  11. {
  12. type: 'paragraph',
  13. children: [
  14. { text: 'It\' so cool.' } // path: [1, 0]
  15. ]
  16. }
  17. ]

虽然Path所代表的路径通常是以顶层Editor作为root节点的,但也会有其他情况,比如由Node提供的get方法中传入的Path参数则代表的是相对路径[源码]

  1. /**
  2. * Get the descendant node referred to by a specific path. If the path is an
  3. * empty array, it refers to the root node itself.
  4. */
  5. get(root: Node, path: Path): Node {
  6. let node = root
  7. for (let i = 0; i < path.length; i++) {
  8. const p = path[i]
  9. if (Text.isText(node) || !node.children[p]) {
  10. throw new Error(
  11. `Cannot find a descendant at path [${path}] in node: ${Scrubber.stringify(
  12. root
  13. )}`
  14. )
  15. }
  16. node = node.children[p]
  17. }
  18. return node
  19. }

Point

Point是在Path的基础上封装而来的概念:

  1. /**
  2. * `Point` objects refer to a specific location in a text node in a Slate
  3. * document. Its path refers to the location of the node in the tree, and its
  4. * offset refers to the distance into the node's string of text. Points can
  5. * only refer to `Text` nodes.
  6. */
  7. export interface BasePoint {
  8. path: Path
  9. offset: number
  10. }
  11. export type Point = ExtendedType<'Point', BasePoint>

用于定位单个字符在文档中的位置;先用Path定位到字符所属的Node,再根据offset字段信息精确到字符是在该Nodetext文本中的偏移量。

我们仍然以前面的示例做说明,如果想要将光标位置定位到第一句话"This is editable rich text!"的感叹号之后,其Point值为:

  1. const initialValue: Descendant[] = [
  2. // path: [0]
  3. {
  4. type: 'paragraph',
  5. children: [
  6. { text: 'This is editable ' }, // path: [0, 0]
  7. { text: 'rich text!', bold: true } // { path: [0, 1], offset: 10 }
  8. ]
  9. },
  10. // path: [1]
  11. {
  12. type: 'paragraph',
  13. children: [
  14. { text: 'It\' so cool.' } // path: [1, 0]
  15. ]
  16. }
  17. ]

Range

最后一个Range则是再在Point基础上延伸封装而来的概念:

  1. /**
  2. * `Range` objects are a set of points that refer to a specific span of a Slate
  3. * document. They can define a span inside a single node or a can span across
  4. * multiple nodes.
  5. */
  6. export interface BaseRange {
  7. anchor: Point
  8. focus: Point
  9. }
  10. export type Range = ExtendedType<'Range', BaseRange>

它代表的是一段文本的集合;包含有两个Point类型的字段anchorfocus。看到这,应该能发现Slate中Range的概念其实与DOM中的Selection对象是一样的,anchorfocus分别对应原生Selection中的锚点anchorNode和焦点focusNode;这正是Slate对于原生Selection做的抽象,使之在自身API中更方便地通过光标选区来获取文档树中的内容。

我们在上一篇文章中有提到过的Editor.selection是一个Selections类型,其本身就是一个Range类型,专门用来指定编辑区域中的光标位置[源码]

  1. export type BaseSelection = Range | null
  2. export type Selection = ExtendedType<'Selection', BaseSelection>

Refs

当我们需要长期追踪某些Node时,可以通过获取对应NodePath/Point/Range值并保存下来以达到目的。但这种方式存在的问题是,在Slate文档树经过insertremove等操作后,原先的Path/Point/Range可能会产生变动或者直接作废掉。

Refs的出现就是为了解决上述问题。

三者的ref的定义分别在slate/src/interfaces/下的path-ref.tspoint-ref.tsrange-ref.ts文件中:

  1. /**
  2. * `PathRef` objects keep a specific path in a document synced over time as new
  3. * operations are applied to the editor. You can access their `current` property
  4. * at any time for the up-to-date path value.
  5. */
  6. export interface PathRef {
  7. current: Path | null
  8. affinity: 'forward' | 'backward' | null
  9. unref(): Path | null
  10. }
  11. /**
  12. * `PointRef` objects keep a specific point in a document synced over time as new
  13. * operations are applied to the editor. You can access their `current` property
  14. * at any time for the up-to-date point value.
  15. */
  16. export interface PointRef {
  17. current: Point | null
  18. affinity: TextDirection | null
  19. unref(): Point | null
  20. }
  21. /**
  22. * `RangeRef` objects keep a specific range in a document synced over time as new
  23. * operations are applied to the editor. You can access their `current` property
  24. * at any time for the up-to-date range value.
  25. */
  26. export interface RangeRef {
  27. current: Range | null
  28. affinity: 'forward' | 'backward' | 'outward' | 'inward' | null
  29. unref(): Range | null
  30. }

都包含以下三个字段:

  • current:同React ref用法一样,用current字段存储最新值
  • affinity:当前定位所代表的节点 在文档树变动时如果受到影响的话,所采取的调整策略
  • unref:卸载方法;彻底删除当前的ref确保能够被引擎GC掉

另外我们先来看下各种RefsSlate存储的方式,跳到slate/src/utils/weak-maps.ts[源码]

  1. export const PATH_REFS: WeakMap<Editor, Set<PathRef>> = new WeakMap()
  2. export const POINT_REFS: WeakMap<Editor, Set<PointRef>> = new WeakMap()
  3. export const RANGE_REFS: WeakMap<Editor, Set<RangeRef>> = new WeakMap()

可以看到Refs的存储区在一个Set数据结构中的;而对于不同EditorSet<xxxRef>的存储则是放在哈希表WeakMap中的,使其不会影响到GC(WeakMap同Map原理一样,都是ES6之后新出的哈希数据结构,与Map不同点在于其持有的引用算作弱引用)。

生成三种Ref以及获取相应Refs的方法定义在EditorInterface接口上[源码]

  1. export interface EditorInterface {
  2. // ...
  3. pathRef: (
  4. editor: Editor,
  5. path: Path,
  6. options?: EditorPathRefOptions
  7. ) => PathRef
  8. pointRef: (
  9. editor: Editor,
  10. point: Point,
  11. options?: EditorPointRefOptions
  12. ) => PointRef
  13. rangeRef: (
  14. editor: Editor,
  15. range: Range,
  16. options?: EditorRangeRefOptions
  17. ) => RangeRef
  18. pathRefs: (editor: Editor) => Set<PathRef>
  19. pointRefs: (editor: Editor) => Set<PointRef>
  20. rangeRefs: (editor: Editor) => Set<RangeRef>
  21. }

PathPointRange三者的实现逻辑都差不多,下面就以Path为例作介绍。

pathRef

  1. /**
  2. * Create a mutable ref for a `Point` object, which will stay in sync as new
  3. * operations are applied to the editor.
  4. */
  5. pointRef(
  6. editor: Editor,
  7. point: Point,
  8. options: EditorPointRefOptions = {}
  9. ): PointRef {
  10. const { affinity = 'forward' } = options
  11. const ref: PointRef = {
  12. current: point,
  13. affinity,
  14. unref() {
  15. const { current } = ref
  16. const pointRefs = Editor.pointRefs(editor)
  17. pointRefs.delete(ref)
  18. ref.current = null
  19. return current
  20. },
  21. }
  22. const refs = Editor.pointRefs(editor)
  23. refs.add(ref)
  24. return ref
  25. }

实现逻辑非常简单,就是根据传入的参数放入ref对象中,并添加卸载方法unref。然后通过pathRefs拿到对应的Set,讲当前的ref对象添加进去。unref方法中实现的则是相反的操作:通过pathRefs拿到对应的Set后,将当前ref对象移除掉,然后再把ref.current的值置空。

pathRefs

  1. /**
  2. * Get the set of currently tracked path refs of the editor.
  3. */
  4. pathRefs(editor: Editor): Set<PathRef> {
  5. let refs = PATH_REFS.get(editor)
  6. if (!refs) {
  7. refs = new Set()
  8. PATH_REFS.set(editor, refs)
  9. }
  10. return refs
  11. }

代码非常简短,类似懒加载的方式做Set的初始化,然后调用get方法获取集合后返回。

Ref同步

前一篇文章我们提到过,用于修改内容的单个Transform方法会包含有多个OperationOperation则是Slate中的原子化操作。而在Slate文档树更新之后,解决Ref同步更新的方式就是:在执行了任意Operation之后,对所有的Ref根据执行的Operation类型做相应的调整。

看到create-editor.ts中的apply方法,该方法是所有Operation执行的入口[源码]

  1. apply: (op: Operation) => {
  2. for (const ref of Editor.pathRefs(editor)) {
  3. PathRef.transform(ref, op)
  4. }
  5. for (const ref of Editor.pointRefs(editor)) {
  6. PointRef.transform(ref, op)
  7. }
  8. for (const ref of Editor.rangeRefs(editor)) {
  9. RangeRef.transform(ref, op)
  10. }
  11. // ...
  12. }

apply方法的最开头就是三组for of循环,对所有的Ref执行对应的Ref.transform方法并传入当前执行的Operation

同样以Path为例,看下path-ref.ts中的PathRef.transform方法[源码]

  1. export const PathRef: PathRefInterface = {
  2. /**
  3. * Transform the path ref's current value by an operation.
  4. */
  5. transform(ref: PathRef, op: Operation): void {
  6. const { current, affinity } = ref
  7. if (current == null) {
  8. return
  9. }
  10. const path = Path.transform(current, op, { affinity })
  11. ref.current = path
  12. if (path == null) {
  13. ref.unref()
  14. }
  15. }
  16. }

将当前ref中的数据和Operation作为参数,传递给对应的Path.transform,返回更新后的path值并赋值给ref.current。如果path为空,则说明当前ref指代的位置已经失效,调用卸载方法unref

至于Path.transform的细节在本篇就不展开了,我们留到后续的Transform篇再统一讲解: )

slate源码解析(三)- 定位的更多相关文章

  1. Celery 源码解析三: Task 对象的实现

    Task 的实现在 Celery 中你会发现有两处,一处位于 celery/app/task.py,这是第一个:第二个位于 celery/task/base.py 中,这是第二个.他们之间是有关系的, ...

  2. Mybatis源码解析(三) —— Mapper代理类的生成

    Mybatis源码解析(三) -- Mapper代理类的生成   在本系列第一篇文章已经讲述过在Mybatis-Spring项目中,是通过 MapperFactoryBean 的 getObject( ...

  3. ReactiveCocoa源码解析(三) Signal代码的基本实现

    上篇博客我们详细的聊了ReactiveSwift源码中的Bag容器,详情请参见<ReactiveSwift源码解析之Bag容器>.本篇博客我们就来聊一下信号量,也就是Signal的的几种状 ...

  4. ReactiveSwift源码解析(三) Signal代码的基本实现

    上篇博客我们详细的聊了ReactiveSwift源码中的Bag容器,详情请参见<ReactiveSwift源码解析之Bag容器>.本篇博客我们就来聊一下信号量,也就是Signal的的几种状 ...

  5. React的React.createRef()/forwardRef()源码解析(三)

    1.refs三种使用用法 1.字符串 1.1 dom节点上使用 获取真实的dom节点 //使用步骤: 1. <input ref="stringRef" /> 2. t ...

  6. slate源码解析(一)- 序言

    笔者从大学时期就开始接触的前端,在刚去实习的时候就被导师安排去做内网的一个小富文本工具.之后从毕业后干的第一份工作游戏客户端,到现在做着可视化相关的前端工作,都有在做富文本相关的内容.可以说是和富文本 ...

  7. slate源码解析(二)- 基本框架与数据模型

    源码架构 首先来看下最核心的slate包下的目录: 可以看到,作为一个开源富文本库,其源码是相当之少.在第一篇文章中说过,Slate没有任何开箱即用的功能,只提供给开发者用于构建富文本所需的最基本的一 ...

  8. Sentinel源码解析三(滑动窗口流量统计)

    前言 Sentinel的核心功能之一是流量统计,例如我们常用的指标QPS,当前线程数等.上一篇文章中我们已经大致提到了提供数据统计功能的Slot(StatisticSlot),StatisticSlo ...

  9. Spring源码解析三:IOC容器的依赖注入

    一般情况下,依赖注入的过程是发生在用户第一次向容器索要Bean是触发的,而触发依赖注入的地方就是BeanFactory的getBean方法. 这里以DefaultListableBeanFactory ...

  10. jQuery 源码解析(三) pushStack方法 详解

    该函数用于创建一个新的jQuery对象,然后将一个DOM元素集合加入到jQuery栈中,最后返回该jQuery对象,有三个参数,如下: elems Array类型 将要压入 jQuery 栈的数组元素 ...

随机推荐

  1. 2023-06-22:一所学校里有一些班级,每个班级里有一些学生,现在每个班都会进行一场期末考试 给你一个二维数组 classes ,其中 classes[i] = [passi, totali] 表

    2023-06-22:一所学校里有一些班级,每个班级里有一些学生,现在每个班都会进行一场期末考试 给你一个二维数组 classes ,其中 classes[i] = [passi, totali] 表 ...

  2. 基于VAE的风险分析:基于历史数据的风险分析、基于实时数据的风险分析

    目录 引言 随着人工智能和机器学习的发展,风险分析已经成为许多行业和组织中不可或缺的一部分.传统的基于经验和规则的风险分析方法已经难以满足现代风险分析的需求,因此基于VAE的风险分析方法逐渐成为了主流 ...

  3. WAMP apache 无法运行 报错could not execult menu item

    wamp:could not execult menu item (internal error)[exception]counld not perform service action:服务器没有及 ...

  4. 1.1 熟悉x64dbg调试器

    x64dbg 是一款开源.免费.功能强大的动态反汇编调试器,它能够在Windows平台上进行应用程序的反汇编.调试和分析工作.与传统的调试器如Ollydbg相比,x64dbg调试器的出现填补了Olly ...

  5. AcWing 4489. 最长子序列题解

    思路 此题较为简单,简述一下思路. 设原始数列为 \(a\). 定义 \(dp\) 数组,初始值都为 \(1\). 遍历数组,如果 \(a[i-1]*2 \leq a[i]\) ,那么 \(dp[i] ...

  6. C语言循环坑 -- continue的坑

    文章目录 前言 一.continue语法 1.continue的作用 2.语法 二.大坑项目 题目 分析 正确写法 三.进坑调试 第一种 第二种 总结 前言 在使用continue和break时,会出 ...

  7. Linux内核笔记(三)内核编程语言和环境

    学习概要: Linux内核使用的编程语言.目标文件格式.编译环境.内联汇编.语句表达式.寄存器变量.内联函数 c和汇编函数之间的相互调用机制Makefile文件的使用方法. as86汇编语言语法 汇编 ...

  8. Blazor中如何呈现富文本/HTML

    将需要显示字符串转换成MarkupString类型 @((MarkupString)htmlString) 参考文献 https://stackoverflow.com/questions/60167 ...

  9. DNS与CDN技术

    参考链接: CDN原理简单介绍 浅析:DNS解析和CDN加速 DNS报文格式解析

  10. centos7安装weblogic

    前言 简介:weblogic是java应用服务器软件的一种,类似于tomcat,但功能更多,适用于大型应用场景. 版本: 系统:centos 7(最小化安装,无图形化界面) jdk: oraclejd ...