slate源码解析(三)- 定位

slate,源码,解析,定位 · 浏览次数 : 0

小编点评

**生成内容的步骤:** 1. **创建路径Ref对象:**根据传入的点、范围、操作等信息创建相应的路径Ref对象。 2. **获取路径Ref的Set:**从`PATH_REFS`中获取所有与当前Editor关联的路径Ref。 3. **循环处理路径Ref:**对每个路径Ref,执行`PathRef.transform`方法进行数据更新。 4. **更新路径Ref的属性:**根据不同的更新方式,修改路径Ref的`current`、`affinity`等属性。 5. **处理路径Ref中的操作:**根据不同的操作类型,调用`PathRef.transform`方法进行数据更新。 6. **更新路径Ref的当前值:**根据`transform`方法的返回值,更新路径Ref的`current`属性。 7. **删除已更新的路径Ref:**在操作完成后,删除相应的路径Ref。 8. **返回更新后的结果:**完成所有操作后,返回更新后的结果。 **示例代码:** ```typescript // ...路径Ref的创建和变换 ... // ...路径Ref的获取和更新 ... // ...处理路径Ref中的操作 ... // ...更新路径Ref的属性 ... // ...删除已更新的路径Ref ... // ...返回更新后的结果 ... ``` **注意:** * `Path.transform`方法中的`op`参数表示操作类型,具体取决于操作类型。 * `PathRef.transform`方法可以返回`null`,表示当前路径已经失效。 * 在更新路径Ref的属性时,确保所有操作类型一致。

正文

接口定义

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

/**
 * The `Location` interface is a union of the ways to refer to a specific
 * location in a Slate document: paths, points or ranges.
 *
 * Methods will often accept a `Location` instead of requiring only a `Path`,
 * `Point` or `Range`. This eliminates the need for developers to manage
 * converting between the different interfaces in their own code base.
 */
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.

interface NodeOptions {
  at?: Location
  match?: (node: Node, path: Location) => boolean
  mode?: 'highest' | 'lowest'
  voids?: boolean
}

Path

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

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

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

const initialValue: Descendant[] = [
    // path: [0]
    {
        type: 'paragraph',
        children: [
            { text: 'This is editable ' },		// path: [0, 0]
            { text: 'rich text!', bold: true }	// path: [0, 1]
        ]
    },
    // path: [1]
    {
        type: 'paragraph',
        children: [
            { text: 'It\' so cool.' }	// path: [1, 0]
        ]
    }
]

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

/**
 * Get the descendant node referred to by a specific path. If the path is an
 * empty array, it refers to the root node itself.
 */
get(root: Node, path: Path): Node {
    let node = root

    for (let i = 0; i < path.length; i++) {
        const p = path[i]

        if (Text.isText(node) || !node.children[p]) {
            throw new Error(
                `Cannot find a descendant at path [${path}] in node: ${Scrubber.stringify(
                    root
                )}`
            )
        }

        node = node.children[p]
    }

    return node
}

Point

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

/**
 * `Point` objects refer to a specific location in a text node in a Slate
 * document. Its path refers to the location of the node in the tree, and its
 * offset refers to the distance into the node's string of text. Points can
 * only refer to `Text` nodes.
 */
export interface BasePoint {
  path: Path
  offset: number
}

export type Point = ExtendedType<'Point', BasePoint>

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

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

const initialValue: Descendant[] = [
    // path: [0]
    {
        type: 'paragraph',
        children: [
            { text: 'This is editable ' },		// path: [0, 0]
            { text: 'rich text!', bold: true }	// { path: [0, 1], offset: 10 }
        ]
    },
    // path: [1]
    {
        type: 'paragraph',
        children: [
            { text: 'It\' so cool.' }	// path: [1, 0]
        ]
    }
]

Range

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

/**
 * `Range` objects are a set of points that refer to a specific span of a Slate
 * document. They can define a span inside a single node or a can span across
 * multiple nodes.
 */
export interface BaseRange {
  anchor: Point
  focus: Point
}

export type Range = ExtendedType<'Range', BaseRange>

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

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

export type BaseSelection = Range | null

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文件中:

/**
 * `PathRef` objects keep a specific path in a document synced over time as new
 * operations are applied to the editor. You can access their `current` property
 * at any time for the up-to-date path value.
 */
export interface PathRef {
  current: Path | null
  affinity: 'forward' | 'backward' | null
  unref(): Path | null
}

/**
 * `PointRef` objects keep a specific point in a document synced over time as new
 * operations are applied to the editor. You can access their `current` property
 * at any time for the up-to-date point value.
 */
export interface PointRef {
  current: Point | null
  affinity: TextDirection | null
  unref(): Point | null
}

/**
 * `RangeRef` objects keep a specific range in a document synced over time as new
 * operations are applied to the editor. You can access their `current` property
 * at any time for the up-to-date range value.
 */
export interface RangeRef {
  current: Range | null
  affinity: 'forward' | 'backward' | 'outward' | 'inward' | null
  unref(): Range | null
}

都包含以下三个字段:

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

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

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

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

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

export interface EditorInterface {
    // ...
    pathRef: (
    	editor: Editor,
    	path: Path,
    	options?: EditorPathRefOptions
  	) => PathRef
    
    pointRef: (
    	editor: Editor,
    	point: Point,
    	options?: EditorPointRefOptions
  	) => PointRef
    
    rangeRef: (
    	editor: Editor,
    	range: Range,
    	options?: EditorRangeRefOptions
  	) => RangeRef
    
    pathRefs: (editor: Editor) => Set<PathRef>
    
    pointRefs: (editor: Editor) => Set<PointRef>
    
    rangeRefs: (editor: Editor) => Set<RangeRef>
}

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

pathRef

  /**
   * Create a mutable ref for a `Point` object, which will stay in sync as new
   * operations are applied to the editor.
   */
  pointRef(
    editor: Editor,
    point: Point,
    options: EditorPointRefOptions = {}
  ): PointRef {
    const { affinity = 'forward' } = options
    const ref: PointRef = {
      current: point,
      affinity,
      unref() {
        const { current } = ref
        const pointRefs = Editor.pointRefs(editor)
        pointRefs.delete(ref)
        ref.current = null
        return current
      },
    }

    const refs = Editor.pointRefs(editor)
    refs.add(ref)
    return ref
  }

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

pathRefs

  /**
   * Get the set of currently tracked path refs of the editor.
   */
  pathRefs(editor: Editor): Set<PathRef> {
    let refs = PATH_REFS.get(editor)

    if (!refs) {
      refs = new Set()
      PATH_REFS.set(editor, refs)
    }

    return refs
  }

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

Ref同步

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

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

apply: (op: Operation) => {
    for (const ref of Editor.pathRefs(editor)) {
        PathRef.transform(ref, op)
    }

    for (const ref of Editor.pointRefs(editor)) {
        PointRef.transform(ref, op)
    }

    for (const ref of Editor.rangeRefs(editor)) {
        RangeRef.transform(ref, op)
    }
    
    // ...
}

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

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

export const PathRef: PathRefInterface = {
  /**
   * Transform the path ref's current value by an operation.
   */
  transform(ref: PathRef, op: Operation): void {
    const { current, affinity } = ref

    if (current == null) {
      return
    }

    const path = Path.transform(current, op, { affinity })
    ref.current = path

    if (path == null) {
      ref.unref()
    }
  }
}

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

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

与slate源码解析(三)- 定位相似的内容:

slate源码解析(三)- 定位

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

React Hooks源码深度解析

React Hooks是React16.8 引入的一个新特性,它允许函数组件中使用state和其他 React 特性,而不必使用类组件。Hooks是一个非常重要的概念,因为它们提供了更简单、更易于理解的React开发体验。本篇文章以React Hooks源码为基,进行深度解析复盘其实现原理。

基于AQS实现自定义同步类

Mutex(互斥锁) Mutex是一个不可重入的互斥锁实现。锁资源(AQS里的state)只有两种状态:0表示未锁定,1表示锁定。下边是Mutex的核心源码: class Mutex implements Lock, java.io.Serializable { // 自定义同步器 private

初探富文本之编辑器引擎

初探富文本之编辑器引擎 在前文中我们介绍了富文本的基础概念,以及富文本的基本发展历程,那么在本文中将会介绍当前主流开源的富文本编辑器引擎。当前使用最广泛的富文本编辑器是L1的富文本编辑器,其能满足绝大部份使用场景,由此也诞生了非常多优秀的开源富文本引擎,这其中有仅提供引擎的编辑器例如Slate.js

一文搞懂RESTful开发

REST(Representational State Transfer),表现形式状态转换,它是一种软件架构风格 当我们想表示一个网络资源的时候,可以使用两种方式: 传统风格资源描述形式 http://localhost/user/getById?id=1 查询id为1的用户信息 http://l

10分钟搞定Mysql主从部署配置

流程 Master数据库安装 Slave数据库安装 配置Master数据库 配置Slave数据库 网络信息 Master数据库IP:192.168.198.133 Slave数据库IP:192.168.198.132 配置Master数据库 在Master数据库安装完毕后,修改/etc/my.cnf

聊聊MassTransit——状态机实现Saga模式(译)

翻译自 [Saga State Machines](https://masstransit.io/documentation/patterns/saga "Saga State Machines") ### Saga State Machines(状态机) > Saga State Machines

VUEX 的使用学习二: state

转载请注明出处: state 提供唯一的数据资源,所有的共享的数据都要统一放到store 中的state中进行存储; 状态state用于存储所有组件的数据。 管理数据 // 初始化vuex对象 const store = new vuex.Store({ state: { // 管理数据 count

浅析静态应用安全测试

摘要:根据Forrester的 The State Of Application Security, 2022一文的预测,应用安全性的缺失将仍然是最常见的外部攻击方式,因此SAST将会在可预见的未来一直被重视。 本文分享自华为云社区《SAST-静态应用安全测试》,作者: gentle_zhou 。

[转帖]Linux查看raid1和raid10分别由哪些盘组成,在哪个槽位

查找有问题的盘 MegaCli64 -PDList -aALL |grep "Firmware state" 6个盘,2个坏了 查看raid级别和硬盘的状态 MegaCli64 -ShowSummary -aALL|grep -i -E "RAID Level| State"|sort raid1,