iT邦幫忙

2021 iThome 鐵人賽

DAY 15
0
Modern Web

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

Day 15. slate × Interfaces × Iteration

https://ithelp.ithome.com.tw/upload/images/20210930/20139359OCWjkp01gi.png

JS 的 Iteration 在 Slate 裡頭佔了不小的份量,即便有 Ref concepts 讓我們得以追蹤特定的 Location value ,在很多時候我們仍會需要透過『遍歷』的方式去實現我們的功能。

在 interfaces/ 裡提供的所有 method apis ,只要名稱是『複數( s 結尾的那種 )』,有 87% 都是透過遍歷的方式實作,而它們除了提供給開發者使用之外也很大幅度地被 Transforms 或 Operations 使用。

我們一樣依照慣例先來介紹 JS 裡與 Iteration 的先備知識後再回頭介紹 Slate 是如何實作這件事的

Protocols


我們首先從 JS 的迭代協議介紹起,在 ES6 的補充內容中,共有兩個針對迭代功能的協議,分別是:可迭代協議( Iterable protocol )、迭代器協議( Iterator protocol )

光是看名字實在很難分辨清楚這兩個 protocols 到底有什麼差異,讓我們依序來介紹它們。

  • 可迭代協議( Iterable protocol )

    這個協議用途是『定義有哪些 JS Object 是可以迭代的』,只有滿足這項協議的 Objects 才能進入 for..of statement 的大門,迭代裡頭的 value

    var a = { first: 1, second: 2 }
    for (let i of a) {
    	console.log('success', i) // Uncaught TypeError: a is not iterable
    }
    
    var b = [1, 2]
    for (let i of b) {
    	console.log('success', i) // success 1 \n success 2
    }
    

    附上 for...of 在 MDN 上的解釋:『 The for...of statement creates a loop iterating over iterable objects 』

    想一想其實也蠻合理的,如果沒有這個協議提供給 for...of ,它又該如何判斷丟進來的 value 能不能做迭代呢?就算沒有協議規範,上例的 code 裡頭的 a 也很明顯不是一個能直接拿來迭代的 object 。

    理解這個協議的用途後,我們又應該如何實作出一個 iterable 的 object 呢?

    我們可以從已經符合協議規範的 Array 中找到一些線索:

    Array.prototype[Symbol.iterator]() // Array Iterator {}
    

    實作的方式就是在 object (或在原型鏈的原型物件)中實作一個擁有 @@iterator key 的 method ,而 @@iterator 就被存在 ES6 的 Symbol.iterator 的回傳值中:

    var a = {
    	...,
    	[Symbol.iterator]: ... // method implementation
    }
    

    丟入 for...of statement 以後它就會自動去搜尋 @@iterator key 回傳的 method 。

    還有另一個限制就是 @@iterator method 必須要回傳一組符合下一個迭代器協議的 Iterator 。

  • 迭代器協議( Iterator protocol )

    這個協議定義了一個迭代器( Iterator )所應具備的內容。

    一個符合規範的迭代器必須要具有 next key 的 method ,而它首先必須不能接受任何參數,另外它必須要回傳一組至少擁有下方兩個屬性的物件:

    • done (布林值)

      • true 代表已迭代完畢整個序列,此時回傳的 object 可以只包含 done 就好
      • false 代表還沒迭代完畢,迭代器仍能產出序列中的下一個值
    • value

      當前回傳的成員值。

    只要符合規範,我們可以任意定義整組迭代器的內容,我們試著來實作看看

    const iteratorEx = {
      start: 1,
      end: 3,
      [Symbol.iterator]() {
        this.current = this.start;
        return this;
      },
      next() {
        if (this.current >= 1 && this.current <= 3) {
          return { done: false, value: this.current++ };
        }
    
        return { done: true };
      },
    };
    
    for (let num of iteratorEx) {
      console.log(num); // 1\n2\n3
    }
    
    1. 我們建立了一組同時符合 Iterable protocol 與 Iterator protocol 的 iteratorEx 物件,將它丟進 for...of statement 時它首先會找到並呼叫 @@iterator key 的 method
    2. 此時 @@iterator method 裡頭的 this 指向 iteratorEx ,建立 current key 並將 value 指向 start 的位置並回傳 iteratorEx 本身
    3. 執行 @@iterator method 回傳的結果,也就是 iteratorEx 裡頭的 next method ,依照裡面定義的邏輯去做迭代並輸出結果。
關於迭代協議還有其他額外的使用方式,有興趣的讀者可以自行前往 MDN 查看,網路上也有許多相關的文章有做介紹,筆者就先不偏題太多了!

Protocols 規範好了,可以去客製化自己所需要的可迭代物件與迭代器確實不錯,但總不能每次都讓開發者去自定義,有個 Javascript 原生提供的工具才合理吧?!


接下來要介紹的 function*Generator 正是為此而存在的。

Generator & function*


這兩個也是在 ES6 同時推出的新內容,開發者在 function* 裡頭定義一組生成器函式( Generator function ),這組函式在呼叫後 『並不會執行函式內容更不會返回函式運算結果,而是回傳一個生成器( Generator )物件』 。

下圖為在 chrome 上的執行結果

https://ithelp.ithome.com.tw/upload/images/20210930/20139359l3mehCgu8k.png

生成器函式裡提供了 yield keyword ,開發者可以透過它暫停函式的執行,並會將其右手邊的表達式結果當作 Iterator protocol 中定義的 value 的值回傳出去,而生成器物件同時符合了 Iterable protocol 以及 Iterator protocol 也因此在呼叫了生成器函式取得生成器物件後便擁有了 next method 可以使用:

function* generatorFn() {
	yield 'First Yielding!!';
	yield 'Second Yielding!!';
	yield 'Third Yielding!!';
}

var generator = generatorFn();
generatorFn().next(); // Object { value: 'First Yielding!!', done: false }
generatorFn().next(); // Object { value: 'Second Yielding!!', done: false }
generatorFn().next(); // Object { value: 'Third Yielding!!', done: false }

每執行一次 next method 就會重啟一次函式的執行,直到遇到下一次的 yield keyword 回傳結果,除非遇到:

  • return keyword 設 value 為右方表達式結果以及將 done 設為 true
  • 執行到函式結束將 value 設為 undefined 以及將 done 設為 true

也就是說 Generator function 是分段執行的,『 yield keyword 負責暫停與句的執行, next method 則會恢復函式的執行』。

除了 next method 以外它也提供 returnthrow 等 methods 。

  • next

    除了重啟函式的執行之外,如果傳變數進去,則會成為當前重啟的 yield 表達式本身的回傳結果:

    function* generatorFn() {
    	let test = yield 'yield';
    	console.log('test-->', test);
    }
    
    var generator = generatorFn();
    console.log(generator.next()); // Object { value: 'yield', done: false }
    console.log(generator.next('testing!!')); // test-->testing!! \n Object { value: undefined, done: true }
    
  • return

    直接返回提供給 method 的參數內容作為 value 並設 donetrue ,並且不會接著繼續執行 Generator function 內容:

    function* generatorFn() {
        let test = yield 'yield';
        console.log('test-->n', test);
    }
    
    var generator = generatorFn();
    console.log(generator.next()); // Object { value: 'yield', done: false }
    console.log(generator.return('return value')); // Object { value: 'return value', done: true }
    
  • throw

    用於向 Generator 內部拋出 Error :

    function* generatorFn() {
      while(true) {
        try {
           yield 42;
        } catch(e) {
          console.log("Error caught!");
        }
      }
    }
    
    var generator = generatorFn();
    generator.next(); // { value: 42, done: false }
    generator.throw(new Error()); // "Error caught!"
    

以上就是對 JS Iteration 的事前介紹,讀者也可以前往 MDN 查看裡頭的介紹,這裡再另外提供一些資源給讀者:

緊接著就到 slate 裡頭是如何搭配 Iteration 去實作遍歷功能的。

*Entry type


只要是與遍歷相關的功能, slate 都是透過 generator 以及 for...of statement 來實作的,它同時定義了幾組 Entry types 作為透過 Generator 執行遍歷功能時 yield keyword 回傳的 type ,分別是 NodeEntryElementEntryPointEntry ,例如當 Generator 要透過 yield 回傳 element value 時, slate 會選擇回傳 ElementEntry 而非 Element type

export interface NodeInterface {
	elements: (
    root: Node,
    options?: {
      from?: Path
      to?: Path
      reverse?: boolean
      pass?: (node: NodeEntry) => boolean
    }
  ) => Generator<ElementEntry, void, undefined>
}

來看一下這三種 Entry types 的 type 是怎麼被定義的吧!首先從 NodeEntryElementEntry 開始:

/**
 * `NodeEntry` objects are returned when iterating over the nodes in a Slate
 * document tree. They consist of the node and its `Path` relative to the root
 * node in the document.
 */

export type NodeEntry<T extends Node = Node> = [T, Path]

/**
 * `ElementEntry` objects refer to an `Element` and the `Path` where it can be
 * found inside a root node.
 */

export type ElementEntry = [Element, Path]

這兩組 Entry types 本質跟用法上都是相似的,差別就只是在 NodeEntry 的使用範圍比較廣而 ElementEntry 僅限縮在 element 的上,筆者大多時候都是使用 NodeEntry 來做遍歷,然後在搭配 match method option 與 statement control 去處理不同 type 的情境。

match method 可以在許多 method apis ,包含 Transform methods 以及 Operations 裡看到它被放在 options 裡面,隨便拿一個 transforms/node.ts 裡的 insertNodes 當作範例:

https://ithelp.ithome.com.tw/upload/images/20210930/20139359V47HOVG8Ug.png

搭配著 NodeMatch type 的它,用途就是提供給開發者一個 narrow Node type 的 helper method ,來看一下 NodeMatch type 裡面的定義就會非常清楚了:

/**
 * A helper type for narrowing matched nodes with a predicate.
 */

export type NodeMatch<T extends Node> =
  | ((node: Node, path: Path) => node is T)
  | ((node: Node, path: Path) => boolean)

接著是最後的 PointEntry ,它的用途就很限縮了,作者留給它的 comment 介紹就說明的非常直白了:它就是拿來 Iterate 一組 range 裡頭的 anchorfocus value

/**
 * `PointEntry` objects are returned when iterating over `Point` objects that
 * belong to a range.
 */

export type PointEntry = [Point, 'anchor' | 'focus']

在 slate 裡僅有 interfaces/range 裡頭的 points method api 使用到 PointEntry 而已,其實它就是去實作 PointEntry 的 comment 描述的功能而已:

/**
 * Iterate through all of the point entries in a range.
 */

*points(range: Range): Generator<PointEntry, void, undefined> {
  yield [range.anchor, 'anchor']
  yield [range.focus, 'focus']
},

來做個統整刷刷存在感好了,今天都沒有我出場的機會。
今天首先從 JS Iterate 的 Protocols 開始介紹起,解釋了 Iterable protocolIterator protocol 之間的差異更了解如何實作出一組同時符合者兩個 Protocols 的JS Object。
接著輪到了 Generator ,我們探討了 yield 這個 keyword 的使用方式以及 Generator 提供的各種 methods 的使用情境。
最後輪到了各種 Entry types 的介紹,再順便了解 match option method 搭配 NodeMatch type 的作用。


緊接著下一篇就要為 Interface 這個章節收尾了!

下一篇要探討的主題是 slate 的 Custom type ,在準備這篇的內容時筆者是非常興奮,最後也獲益良多的!因為它主要的 code 只有短短不到 10 行而已,卻獨自包攬了 slate 所有的 custom types 定義的功能,真的非常之厲害!

我們也會從它的歷史小故事開始介紹起,明天再見吧~


上一篇
Day 14. slate × Interfaces × Ref
下一篇
Day 16. slate × Interfaces × CustomType
系列文
深入 slate.js x 一起打造專屬的富文字編輯器吧!30

尚未有邦友留言

立即登入留言