iT邦幫忙

2021 iThome 鐵人賽

DAY 25
0
Modern Web

深入 slate.js x 一起打造專屬的富文字編輯器吧!系列 第 25

Day 25. slate × Normalizing × Dirty-Path

https://ithelp.ithome.com.tw/upload/images/20211010/20139359T1BZp6TFjz.png

我們在 上上一篇 也有提到過 slate 將 Normalizing 這項功能的實現拆成了第一步驟的『骯髒標記( Dirty-Path ) 』以及透過骯髒標記實際實行『正規化( Normalize )』的第二步驟。

順序都分出來了我們當然先來看一下骯髒標記到底是哪裡被弄髒了。

Dirty-Path :你才髒,你全家都ㄗ...


首先我們在 weak-map.ts 裡可以看到一組名叫 DIRTY_PATHSWeakMap

// weak-map.ts
export const DIRTY_PATHS: WeakMap<Editor, Path[]> = new WeakMap()

它負責紀錄編輯器當前的骯髒路徑內容,供後續的 methods 使用。

如果讀者還記得我們在 Day21 貼的 apply method 的 code 的內容的話就會發現其中設定 Dirty-Paths 的 section 裡又拆成了 oldDirtyPathsnewDirtyPaths 兩個變數,並針對這兩組變數額外進行一些處理後才整合進 DIRTY_PATHS 裡。

這麼做是因為 Normalize 的執行方式並不是『一個 Operation 搭配一次 Normalize 』,而是『一組完整的 FLUSHING 搭配一次 Normalize 』,想想看一次 Transform 裡有好多次的 Operations ,真要是這樣實作的話該有多耗效能啊!才剛弄乾淨馬上就又被弄髒了

而不同的 Operation 有機會生成不同的骯髒路徑,也因此 slate 需要這項機制為它整理出一組 FLUSHING 裡最後確定需要 Normalize 的 Dirty Paths ,一樣來看看它是怎麼運作的:

const set = new Set()
const dirtyPaths: Path[] = []

const add = (path: Path | null) => {
  if (path) {
    const key = path.join(',')

    if (!set.has(key)) {
      set.add(key)
      dirtyPaths.push(path)
    }
  }
}

const oldDirtyPaths = DIRTY_PATHS.get(editor) || []
const newDirtyPaths = getDirtyPaths(op)

for (const path of oldDirtyPaths) {
  const newPath = Path.transform(path, op)
  add(newPath)
}

for (const path of newDirtyPaths) {
  add(path)
}

DIRTY_PATHS.set(editor, dirtyPaths)

在每一次的 Operation 中:

  • oldDirtyPaths 會去取得儲存在 DIRTY_PATHS 裡頭前一次的結果,經過這次的 operation transform 為正確的 path 以後經由 add method 推入 dirtyPaths 變數裡。
  • newDirtyPaths 會透過 getDirtyPaths 取得這次 operation 會製造出的 Dirty-Path 並經由 add method 推入 dirtyPaths 變數裡。
  • add method 會將丟入的 path 與第一行的 set 比對,只推入還不存在於 dirtyPaths 變數裡的 path 以避免重複推入。
  • 最後將 dirtyPaths 存為 DIRTY_PATHSeditor 的 value 。

Dirty-Path Generation


與 Dirty-Path 生成相關的一切邏輯都被封裝在 getDirtyPaths 這個 helper function 裡。

除了 SetSelectionOperationSetNodeOperation 之外,其他 Operations 都會生成 Dirty-Path 。

const getDirtyPaths = (op: Operation): Path[] => {
  switch (op.type) {
    case 'insert_text': ... // Implementation
    case 'remove_text': ... // Implementation
    case 'set_node': ... // Implementation
    case 'insert_node': ... // Implementation
    case 'merge_node': ... // Implementation
    case 'move_node': ... // Implementation
		case 'remove_node': ... // Implementation
    case 'split_node': ... // Implementation
    default: {
      return []
    }
  }
}

一個一個來看他們的判斷標記的概念吧:

  • insert_textremove_textset_node

    這三組 Operations 會標記的骯髒路徑一樣都是『一路從根節點串連下來的祖先路徑與 op.path 本身的路徑』:

    case 'insert_text':
    case 'remove_text':
    case 'set_node': {
      const { path } = op
    
      /**
        Path.levels: Get a list of paths at every level down to a path. Note: this
        is the same as `Path.ancestors`, but including the path itself.
      */
      return Path.levels(path)
    }
    

    https://ithelp.ithome.com.tw/upload/images/20211010/20139359FwiVuzjzpr.png

  • insert_node

    受到骯髒標記的路徑包含: op.path 的祖先路徑、 path 本身、 path 的子層路徑:

    case 'insert_node': {
      const { node, path } = op
      const levels = Path.levels(path)
      const descendants = Text.isText(node)
        ? []
        : Array.from(Node.nodes(node), ([, p]) => path.concat(p))
    
      return [...levels, ...descendants]
    }
    

    https://ithelp.ithome.com.tw/upload/images/20211010/20139359xxgdbZvyjN.png

  • merge_node

    受到骯髒標記的路徑包含: op.path 的祖先路徑、 path 同層的前一個 sibling (因為是向前合併,所以只需要標記前一個 sibling 而不需要標記 path 本身)

    case 'merge_node': {
      const { path } = op
      const ancestors = Path.ancestors(path)
      const previousPath = Path.previous(path)
      return [...ancestors, previousPath]
    }
    

    https://ithelp.ithome.com.tw/upload/images/20211010/20139359Xpnnd8RcDK.png

  • move_node

    受到骯髒標記的路徑包含:

    • 欲移動路徑經由 Path.transform 更新過後的所有祖先路徑

      const oldAncestors: Path[] = []
      
      for (const ancestor of Path.ancestors(path)) {
        const p = Path.transform(ancestor, op)
        oldAncestors.push(p!)
      }
      
    • 移動到的目標路徑經由 Path.transform 更新過後的所有祖先路徑

      const newAncestors: Path[] = []
      
      for (const ancestor of Path.ancestors(newPath)) {
        const p = Path.transform(ancestor, op)
        newAncestors.push(p!)
      }
      
    • 最後是移動結果的路徑

      const newParent = newAncestors[newAncestors.length - 1]
      const newIndex = newPath[newPath.length - 1]
      const resultPath = newParent.concat(newIndex)
      

    將上述三組路徑們包成一組陣列回傳回去

    return [...oldAncestors, ...newAncestors, resultPath]
    

    https://ithelp.ithome.com.tw/upload/images/20211010/20139359fQE6n4sKWr.png

  • remove_node

    受到骯髒標記的路徑為被移除之路徑的祖先路徑

    case 'remove_node': {
      const { path } = op
      const ancestors = Path.ancestors(path)
      return [...ancestors]
    }
    
  • split_node

    受到骯髒標記的路徑包含: op.path 的祖先路徑、 path 本身、與 path 同層的後一個 sibling

    case 'split_node': {
      const { path } = op
      const levels = Path.levels(path)
      const nextPath = Path.next(path)
      return [...levels, nextPath]
    }
    

明天就到了本章節的最後一篇了,介紹完第一步驟的骯髒標記以後當然就輪到了實際執行正規化的第二步驟了。

要介紹的內容其實也算是整個 slate 裡數一數二複雜的,為了因應正規化所會遭遇到的問題以及整體的設計與效能上的考量,作者將第二步驟拆成了好幾層 function 彼此各司其職,當初筆者在研究時也是在各個函式之間穿梭,上滑下滑滑到懷疑人生 ...

那麼今天的文章就到這邊為止,咱們明天再見囉~

鐵人賽倒數 5 天

上一篇
Day 24. slate × Normalizing
下一篇
Day 26. slate × Normalizing × normalizeNode
系列文
深入 slate.js x 一起打造專屬的富文字編輯器吧!30

尚未有邦友留言

立即登入留言