JS 的 Iteration 在 Slate 裡頭佔了不小的份量,即便有 Ref concepts 讓我們得以追蹤特定的 Location value ,在很多時候我們仍會需要透過『遍歷』的方式去實現我們的功能。
在 interfaces/ 裡提供的所有 method apis ,只要名稱是『複數( s 結尾的那種 )』,有 87% 都是透過遍歷的方式實作,而它們除了提供給開發者使用之外也很大幅度地被 Transforms 或 Operations 使用。
我們一樣依照慣例先來介紹 JS 裡與 Iteration 的先備知識後再回頭介紹 Slate 是如何實作這件事的
我們首先從 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
}
iteratorEx
物件,將它丟進 for...of
statement 時它首先會找到並呼叫 @@iterator
key 的 method@@iterator
method 裡頭的 this 指向 iteratorEx
,建立 current
key 並將 value 指向 start
的位置並回傳 iteratorEx
本身@@iterator
method 回傳的結果,也就是 iteratorEx
裡頭的 next
method ,依照裡面定義的邏輯去做迭代並輸出結果。Protocols 規範好了,可以去客製化自己所需要的可迭代物件與迭代器確實不錯,但總不能每次都讓開發者去自定義,有個 Javascript 原生提供的工具才合理吧?!
接下來要介紹的 function*
與 Generator
正是為此而存在的。
這兩個也是在 ES6 同時推出的新內容,開發者在 function*
裡頭定義一組生成器函式( Generator function ),這組函式在呼叫後 『並不會執行函式內容更不會返回函式運算結果,而是回傳一個生成器( Generator )物件』 。
生成器函式裡提供了 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 以外它也提供 return
、 throw
等 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
並設 done
為 true
,並且不會接著繼續執行 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 去實作遍歷功能的。
只要是與遍歷相關的功能, slate 都是透過 generator 以及 for...of
statement 來實作的,它同時定義了幾組 Entry types 作為透過 Generator 執行遍歷功能時 yield
keyword 回傳的 type ,分別是 NodeEntry
、 ElementEntry
、 PointEntry
,例如當 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 是怎麼被定義的吧!首先從 NodeEntry
與 ElementEntry
開始:
/**
* `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
當作範例:
搭配著 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 裡頭的 anchor
與 focus
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 protocol
與Iterator 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 定義的功能,真的非常之厲害!
我們也會從它的歷史小故事開始介紹起,明天再見吧~