作って分かるJavaScriptでデータバインド
January 4, 2014
この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。
はじめに
JavaScriptにおけるデータバインドの実装方法を双方向データバインドライブラリのKnockoutJSのソースコードを読み、自分で以下のような最低限の機能を実装をしていきます。
- 変更を
subscribe
できるobservable
オブジェクト - 他の
observable
オブジェクトの値の変更を検知して、自身の値を変えるdependentObservable
オブジェクト
上記の機能を実現するために、KnockoutJSの以下のオブジェクトの仕組みを解析しました。
- ko.subscribable
- ko.observable
- ko.dependencyDetection
- ko.dependentObservable(ko.computed)
この記事のソースコードはgithubで提供されています。
それぞれの機能をいくつかのステップに分けて説明します。各ステップはstep0
からディレクトリに分けられています。ステップ毎にスケルトンコードと説明(README.md
)があります。スケルトンコードにはTODO
が書かれており、埋める事でコードを完成させることが出来ます。なお、各ステップの解答例はanswers
以下に同じディレクトリ構成で置かれています。
なお、この内容はKnockoutJSについて11月のALM(社内勉強会)で発表したものを基にしています。 勉強会のスライドはこちら。
STEP 0: 前準備
まずは、前準備をしましょう。
このステップでは、特にTODO
はありません。
他のステップで使う、いくつかの関数やオブジェクトを用意しましょう。
はじめに、グローバルオブジェクトを極力汚さないように、名前空間を用意しましょう。
名前空間の名前は特に意味はありませんが、KnockoutJSのko
をマネして、2文字くらいが使いやすいでしょう。
今回は、無作為に2文字選びました。
var go = {};
つぎに、デバッグ用のlog
関数を用意しましょう。
console.log
を使ってログをコンソールに出力し、ブラウザを使った場合には、JSON形式でHTMLのリストでも出力する関数です。
var global = this;
// log
(function (selector) {
function log() {
console.log.apply(console, arguments);
var args = [].slice.apply(arguments);
var logStr = (args.length > 1)? JSON.stringify(args) : JSON.stringify(args[0]);
if (selector && global.document) {
var lst = document.querySelector(selector + '>' + 'ul');
if (!lst) {
lst = document.createElement('ul');
document.querySelector(selector).appendChild(lst);
}
lst.innerHTML += '<li>'+logStr+'</li>';
}
}
go.log = log;
})('#result');
さいごに、継承を行なうための関数を用意しましょう。
go.extend
は第1引数で渡したオブジェクトのプロパティを第2引数で渡したオブジェクトにコピーする関数です。継承とはちょっと異なりますが、ここでは簡単の為にこうしておきましょう。この関数は深くコピーすることに注意しておきましょう。コピーしたプロパティのオブジェクト(配列)は別のオブジェクト(配列)となります。
// extend
(function () {
function extend(_super, sub) {
for (var k in _super) {
if (!_super.hasOwnProperty(k)) {
continue;
}
if (Array.isArray(_super[k])) {
sub[k] = _super[k].slice(0);
for(var i = 0; i < _super[k].length; i++) {
if (typeof _super[k][i] === 'object') {
sub[k][i] = {};
extend(_super[k][i], sub[k][i]);
}
}
} else if (typeof _super[k] === 'object') {
sub[k] = {};
extend(_super[k], sub[k]);
} else {
sub[k] = _super[k];
}
}
}
go.extend = extend;
})();
STEP 1: subscribe
とnotify
バインディングの魔法
私はバインディングという機能が好きで、JavaFXやAngularJS、そしてKnockoutJSのバインディングを色々ためしました。また、面白そうだったので、自分でバインディングライブラリを作ってみたりしました。
KnockoutJSのバインディングはHTMLとJSのオブジェクトを双方向バインドすることができます。双方向なので、片方が変更されるともう片方も変更されます。
たとえば、以下の例を見てみると、input
タグのvalue
に、vm.count
がバインディングされています。vm.count
の値は、タイマーでカウントアップされ、その結果がinput
のvalue
へと反映されます。また、input
のvalue
をフォームから変更することもでき、その変更はvm.count
へと反映されます。
counter: <input type="number" id="counter" data-bind="value: count" />
var vm = {
count: ko.observable(0),
};
ko.applyBindings(vm, document.body);
setInterval(function() {
vm.count(parseInt(vm.count(), 10) + 1);
}, 1000);
上記の例を見ると、KnockoutJSのバインディングは魔法のようにオブジェクトとHTMLをバインドしているようです。
もちろん、魔法などではありません。バインドされているもう一方に変更を伝える仕組みが働いていて、変更に追従しているのです。
この変更を伝える仕組みをKockoutJSでは、ko.subcribable
が担っています。
KnockoutJSでメインに使われるko.observable
やko.observableArray
、ko.computed
(ko.dependentObservable
のエイリアス)はこのオブジェクトの機能を継承しています。
subscribe
とnotify
KnockoutJSのko.subcribable
は変更を伝える役目を担っており、以下の2つの機能から成っています。
subscribe
:変更を通知してもらうオブジェクト(subscripter
)を登録するnotifySubscripter
:登録しているsubscripter
に通知する
KnockoutJSのソースコードを参考にしながら、TODO
を埋めていきましょう。
https://github.com/knockout/knockout/blob/v3.0.0/src/subscribables/subscribable.js
// subscribable (function () { var subscribable = { _subscriptions: [], subscribe: function (callback, callbackTarget) { var _subscribable = this; var subscription = { callback: /* TODO: callbackTargetがあったらバインドしてコールバックを設定*/, dispose: function () { // TODO: 破棄したというフラグを立てておく // TODO: _subscribable._subscriptionsの中に自身(this)を発見したら配列から取り除く }, isDisposed: false }; this._subscriptions.push(subscription); return subscription; }, notifySubscripters: function (valueToNotify) { var _subscriptions = this._subscriptions.slice(0); for (var i = 0; i < _subscriptions.length; i++) { var subscription = _subscriptions[i]; // TODO: disposeされていなかったら、callback呼び出す } } }; go.subscribable = subscribable; })();
subscription
はcallback
とdispose
というプロパティを持っており、callback
は引数で渡されたcallback
にcallbackTarget
をFunction.bind
したものです。一方、dispose
は_subscriptoions
から自身を取り除く処理を行なう関数です。_subscriptions
から取り除いただけでは、subscription
オブジェクトから分からないので、フラグを立てておきます。subscribe
はsubscription
を返すので、登録を破棄したければ、dispose
を呼べば破棄されます。
つづいて、notifySubscripters
を考えてみましょう。notifySubscripters
は、_subscriptions
に登録されているsubscription
のcallback
を呼び出す事によって、subscript
しているオブジェクト(subscripter
)に通知を送るメソッドです。notifySubscripters
の引数は通知する値です。引数で渡された値はsubscription
のcallback
関数の引数として渡されます。
TODO
をすべて埋めたらindex.htmlをブラウザで開いて、以下のテストプログラムの動作を確認してみましょう。
// Test
(function () {
// 継承してみる
var subscribable = {};
go.extend(go.subscribable, subscribable);
// 通知がきたら、ログに出力する
var subscription = subscribable.subscribe(function (value) {
go.log(value);
});
// subscripterに'hoge'を通知する
subscribable.notifySubscripters('hoge');
// disposeして破棄する
subscription.dispose();
// subscripterに'piyo'を通知する
// しかし、subscriptionはdisposeされているので、通知されない
subscribable.notifySubscripters('piyo');
})();
以下のように出力されているはずです。
- “hoge”
コメントにも書かれていますが、1回目のnotifySubscripters
では、subscription
のcallback
に通知されます。しかし、2回目は通知するsubscripter
がないので、どのオブジェクトにも通知されません。
STEP 2: 値の更新を通知するオブジェクト
observable
オブジェクト
subscribable
オブジェクトは、通知の登録(subscribe
)と通知(notify
)はできましたが、値そのものを保持することはできませんでした。
そこで、このステップでは内部に値を保持できるobservable
オブジェクトを作る関数を実装してみましょう。
KnockoutJSのko.observable
は、内部に値を保持できるオブジェクトを作る関数です。このオブジェクトは、関数オブジェクトで引数を渡して呼び出すと書き込み(write)になり、引数なしで呼び出すと読み出し(read)になります。ko.observable
では、内部に保持する値の変更をsubscribe
することで、検知することができます。subscribe
の機能は、ko.subscribable
を継承することで実現しています。つまり、書き込み時にnotifySubscripters
を実行する事で、書き込みを通知します。具体的な使い方を見てみましょう。
var foo = ko.observable(100);
foo.subscribe(function(v) {
alert(v);
});
// 200とalertされる
foo(200);
// 200と出力される
console.log(foo());
上記の例ではvar foo = ko.observable(100);
で初期化が行なわれ、observable
オブジェクトが作られています。この時初期値として、100
が設定されています。つづいて、subcribable
で、変更を検知して、alert
を呼び出すコールバック仕掛けます。foo(200)
で書き込みが行なわれると、仕掛けたコールバックが呼ばれて、alert
が行なわれます。foo()
のように引数なしで呼ばれると、内部に保持している値をそのまま返すため、コンソールに200
が出力されます。
それでは、KnockoutJSのソースコードを参考にしながら、TODO
を埋めていきましょう。
https://github.com/knockout/knockout/blob/v3.0.0/src/subscribables/observable.js
// observable (function () { var observable = function (initialValue) { var lastValue = initialValue; function observable() { if (/* TODO: 引数があれば書き込み */) { // write var newValue = arguments[0]; // TODO: 値が変わっていれば、更新して、nofityする } else { // read return lastValue; } } // TODO: go.subscribableを継承する // TODO: observableなオブジェクトを返す }; go.observable = observable; })();
observable
オブジェクト(値が保持できて、変更の検知ができる)を作る関数をgo.observable
とします。go.observable
の引数は、observable
オブジェクトに設定する初期値を受取りましょう。戻り値はもちろんobservable
オブジェクトです。
observable
という関数が2つ出てきてややこしいですが、外側の関数(go.observable
)はko.observable
にあたる関数で、内側の関数が実際に値を保持するobservable
なオブジェクトになります。go.observable
が呼び出される度に、内側のobservable
関数(observable
オブジェクト)が作られます。lastValue
はobservable
オブジェクト内に保持する値を入れる変数で、go.observable
の引数で貰った値を初期値としています。通知する機能はgo.subscribable
で作っているため、observable
オブジェクトは、go.subscribable
を継承しています。
observable
オブジェクトは2種類の呼ばれ方をしています。ひとつめがfoo(200)
のような引数がある呼ばれ方、ふたつめがfoo()
のような引数がない呼ばれ方です。前者が書き込みで後者が読み出しです。JavaScriptの関数の中では、arguments
という引数を保持する配列っぽい特別なオブジェクトが使えます。arguments
は、複数のパターンの引数をとる場合によく使われます。この場合は、arguments.length
を見て、引数の有無で書き込み処理と読込み処理を分ければ良いでしょう。読み込み処理の場合は、単に最後に設定された値(lastValue
)を返すだけでいいでしょう。書き込み処理も、第1引数の値をlastValue
に入れれば良さそうです。書き込み処理では、もうひとつ大切な処理を行なう必要があります。新しい値が設定された場合、その事をsubscripter
に通知する処理です。これはgo.subscribable
から継承したnotifySubscripters
を使えば済みます。
TODO
をすべて埋めたらindex.htmlをブラウザで開いて、以下のテストプログラムの動作を確認してみましょう。
// Test
(function () {
var observable = go.observable('hoge');
var count = 0;
var subscription = observable.subscribe(function () {
count++;
go.log(observable(), count);
});
observable('foo');
observable('foo');
observable('bar');
subscription.dispose();
observable('piyo');
})();
以下のように出力されているはずです。
- [“foo”,1]
- [“bar”,2]
値が新しい値に変更された場合のみ、通知が行なわれていることが分かります。
STEP 3: 依存関係の検出とdependencyDetection
依存関係の検出
KnockoutJSには、ko.computed
という他のko.observable
の値に依存したようなobservable
なオブジェクトがあります。ko.computed
は以下のように使います。
<div id="result"></div>
var hoge = ko.observable(100);
var piyo = ko.observable(200);
var foo = ko.computed(function(){
return hoge() + piyo();
});
foo.subscribe(function(v) {
document.querySelector('#result').innerText = v;
});
hoge(300);
foo
の値(引数なしで評価された値)は、hoge
がpiyo
が変更された場合、自動的に更新されます。もちろん、subscribe
しておけば、その変更は通知されます。
不思議ですよね?KnockoutJSはどのようにして、foo
がhoge
とpiyo
に依存していることを知っているのでしょう?
この不思議を解明するために、以下のように、ko.computed
に渡す関数が評価される回数を数えてみましょう。
<div id="result"></div>
var hoge = ko.observable(100);
var piyo = ko.observable(200);
var count = 0;
var foo = ko.computed(function () {
count++;
document.querySelector('#result').innerText = count;
return hoge() + piyo();
});
hoge(300);
直感的には、hoge(300)
の変更の時に1度呼ばれているだけの様な気がしますが、結果は2
となっています。何故でしょう?
実は、この関数はko.computed
に渡されたときに、1度評価されます。そして、その評価の時に評価されたobservable
オブジェクト(ここでは、hoge
とpiyo
)を記録することで、依存関係を検知しています。また、その時の評価値を初期値として設定しています。
これはKnockoutJSを使う上では非常に大切なことです。なぜならば、このko.computed
に渡す関数内で副作用のある処理をすると、意図しない結果になる場合があるからです。事前に呼び出されることをちゃんと知った上で、使わないと思わぬバグになってしまいます。
dependencyDetection
オブジェクト
KnockoutJSのko.dependencyDetection
は、前述の依存関係の検出を行なうオブジェクトです。ko.dependencyDetection.begin
を実行した後に、評価されたobsrvable
オブジェクトを登録します。dependencyDetection
オブジェクトには、依存関係の検出を始めるbegin
メソッドと、依存関係にsubscribable
オブジェクトを追加するregisterDependency
メソッドが必要です。また、go.observable
の方にも修正が必要です。読込みの場合に、依存関係を記録する必要があるため、引数なしで呼び出された場合の処理に追加しなければなりません。
それでは、KnockoutJSのソースコードを参考にしながら、TODO
を埋めていきましょう。
https://github.com/knockout/knockout/blob/v3.0.0/src/subscribables/dependencyDetection.js
// observable (function () { var observable = function (initialValue) { var lastValue = initialValue; function observable() { if (arguments.length > 0) { // write var newValue = arguments[0]; if (newValue !== lastValue) { lastValue = newValue; observable.notifySubscripters(lastValue); } } else { // read // TODO: 依存関係に追加(go.dependencyDetectionを使う) return lastValue; } } go.extend(go.subscribable, observable); return observable; }; go.observable = observable; })(); // dependencyDetection (function () { var distinctDependencies, callback; var dependencyDetection = { begin: function (_callback) { // TODO: callbackを初期化 // TODO: distinctDependenciesを初期化 }, registerDependency: function (subscribable) { // TODO: すでに依存関係にあった場合と1度もbeginが呼ばれていない場合は無視 // TODO: subscribableを依存関係に追加 // TODO: コールバックが設定してあったら呼び出す } }; go.dependencyDetection = dependencyDetection; })();
TODO
をすべて埋めたらindex.htmlをブラウザで開いて、以下のテストプログラムの動作を確認してみましょう。
// Test
(function () {
var a = go.observable(100);
var b = go.observable(200);
var observables = [];
go.dependencyDetection.begin(function(observable) {
if (a === observable) {
go.log('a');
} else if (b === observable) {
go.log('b');
} else {
go.log(observable);
}
});
a();
a();
b();
})();
以下のように出力されているはずです。
- “a”
- “b”
observable
オブジェクトの値が読み込まれた時に、go.dependencyDetection.registerDependency
が呼び出されているのが分かります。また、1度登録されたobservable
オブジェクトは重複して登録されていません。これで十分に動くように見えますが、実はうまく動きません。しかし、dependentObservable
(computed
)を実装する際に、必要となってくるので、ここはこのまま進みましょう。
STEP 4: 計算結果の変更を検出する
dependentObservable
オブジェクト
前述の通り、KnockoutJSにはko.computed
関数を使うと他のobservable
オブジェクトを使って計算した結果を自身の値とするdependentObservable
オブジェクトを作ることができます。また、dependentObservable
オブジェクトが他のdependentObservable
に依存しても問題ありません。もちろん、通常のobservable
オブジェクトと同様に、subscribe
できる必要があります。そのため、以下のように読込み処理の時に毎回値を評価する方法では、効率が悪く、subscribe
しても依存するobservable
オブジェクトの値が更新されても、次の読込み処理が走るまでnotify
することができません。
// dependentObservable
(function () {
var lastValue;
var dependentObservable = function (readFunction) {
function dependentObservable() {
if (arguments.length > 0) {
// write
throw new Error('Cannot write to computed');
} else {
// read
var newValue = readFunction();
if (newValue !== lastValue) {
lastValue = newValue;
dependentObservable.notifySubscripters(lastValue);
}
go.dependencyDetection.registerDependency(dependentObservable);
return lastValue;
}
}
go.extend(go.subscribable, dependentObservable);
return dependentObservable;
};
go.dependentObservable = dependentObservable;
go.computed = dependentObservable;
})();
依存関係を検出するには、STEP 3で作成したgo.dependencyDetection
を使います。検出された依存するobservable
オブジェクトをsubscribe
して変更された場合にreadFunction
を読み込みlastValue
を更新します。
それでは、KnockoutJSのソースコードを参考にしながら、TODO
を埋めていきましょう。
https://github.com/knockout/knockout/blob/v3.0.0/src/subscribables/dependentObservable.js
// dependentObservable (function () { var dependentObservable = function (readFunction) { var lastValue = null; var isBeingEvaluated = false; var hasBeenEvaluated = false; var subscriptionsToDependencies = []; function evaluateImmediate() { // TODO: evaluateImmediateが既に呼び出し済み(呼び出し済みフラグがtrue)なら何もしない // TODO: 呼び出し済みフラグをtrueにする // TODO: 記録しているsubscriptionsをdisposeして配列を空にする go.dependencyDetection.begin(function (subscribable) { // TODO: 検知されたsubscribableをsubscribeして、そのsubscriptionを記録 }); var newValue = readFunction(); // TODO: 1度でも評価されているかどうかを表すフラグをtrueにする if (newValue !== lastValue) { lastValue = newValue; dependentObservable.notifySubscripters(lastValue); } // TODO: 呼び出し済みフラグをfalseにする } function dependentObservable() { if (arguments.length > 0) { // write throw new Error('Cannot write to computed'); } else { // read if (/* TODO: 一度も評価されていない場合 */) { evaluateImmediate(); } go.dependencyDetection.registerDependency(dependentObservable); return lastValue; } } go.extend(go.subscribable, dependentObservable); // TODO: 最初の評価と初期値の設定 return dependentObservable; }; go.dependentObservable = dependentObservable; go.computed = dependentObservable; })();
evaluateImmediate
はreadFunction
を呼び出し、保持しているobservable
の値を更新します。また、その際にgo.dependencyDetection
を使って、依存するsubscribable
(observable
)を検知します。検知したsubscribable
のsubscribe
する事で、更新された時に再度evaluateImmediate
を呼び出されるようにします。evaluateImmediate
が呼び出される度に、検知した依存関係を破棄し、subscription
も破棄(dispose
)するために、配列subscriptionsToDependencies
で保持しています。dependentObservable
オブジェクトもobservable
オブジェクトの一種なので、値が変更された場合はnotifySubscripters
で変更を通知します。hasBeenEvaluated
は、一番最初の場合は問答無用でevaluateImmediate
を呼び出すために使われます。
TODO
をすべて埋めたらindex.htmlをブラウザで開いて、以下のテストプログラムの動作を確認してみましょう。
// Test
(function () {
var a = go.observable(100);
var b = go.observable(200);
var c = go.computed(function () {
return a() + b();
});
go.log(a(), b(), c());
a(400);
go.log(a(), b(), c());
})();
以下のように出力されているはずです。
- [100,200,300]
- [400,200,600]
うまく動いているように見えます。しかし、これではうまく動かない場合があります。 次のステップでその部分を改良しましょう。
STEP 5: 問題点の改善
このステップでは、STEP 4での実装をうまく動かない例を示して、問題点を改善していきます。
入れ子にした場合
STEP 4までの実装では、以下のようにcomputed
が入れ子になっている場合にうまく動作しません。最初の出力では、100 200
、2番目の出力では150 300
と出力されて欲しいところですが、実際には2番目の出力は150 200
となります。STEP 4までの実装では、readFunction
の中でgo.dependenctyDetection.begin
を多重に呼び出すことを前提としていません。そのため、2度begin
を呼び出すと前回の呼び出しの分のdistinctDependencies
が消えてしまいます。そこで、distinctDependencies
をスタックで管理するようにしましょう。スタックで管理すれば、入れ子に呼び出されても問題ありません。
var a = go.observable(100);
var b = go.computed(function () {
var c = go.computed(function () {
return a() * 2;
});
return c();
});
go.log(a(),b());
a(150);
go.log(a(),b());
それでは、KnockoutJSのソースコードを参考にしながら、TODO
を埋めていきましょう。
- https://github.com/knockout/knockout/blob/v3.0.0/src/subscribables/dependencyDetection.js
https://github.com/knockout/knockout/blob/v3.0.0/src/subscribables/dependentObservable.js
// dependencyDetection (function () { var frames = []; var dependencyDetection = { begin: function (callback) { // TODO: 新しいframeをpushして、依存関係の検出を開始する。callbackが無かったら、undefinedをpushする }, end: function () { // TODO: 先頭のframeをpopして、依存関係の検出を終了する }, registerDependency: function (subscribable) { if (frames.length > 0) { var topFrame = frames[frames.length - 1]; // TODO: topFrameがundefinedの場合とすでにsubscribableが登録してあったら何もしない topFrame.distinctDependencies.push(subscribable); topFrame.callback(subscribable); } } }; go.dependencyDetection = dependencyDetection; })(); // dependentObservable (function () { var dependentObservable = function (readFunction) { var lastValue = null; var isBeingEvaluated = false; var hasBeenEvaluated = false; var subscriptionsToDependencies = []; function evaluateImmediate() { if (isBeingEvaluated) { return; } isBeingEvaluated = true; for (var i = 0; i < subscriptionsToDependencies.length; i++) { subscriptionsToDependencies[i].dispose(); } subscriptionsToDependencies = []; go.dependencyDetection.begin(function (subscribable) { subscriptionsToDependencies.push(subscribable.subscribe(evaluateImmediate)); }); var newValue = readFunction(); hasBeenEvaluated = true; if (newValue !== lastValue) { lastValue = newValue; dependentObservable.notifySubscripters(lastValue); } // TODO: 依存関係の検出を終了する isBeingEvaluated = false; } function dependentObservable() { if (arguments.length > 0) { // write throw new Error('Cannot write to computed'); } else { // read if (!hasBeenEvaluated) { evaluateImmediate(); } go.dependencyDetection.registerDependency(dependentObservable); return lastValue; } } go.extend(go.subscribable, dependentObservable); evaluateImmediate(); return dependentObservable; }; go.dependentObservable = dependentObservable; go.computed = dependentObservable; })();
スタックをframes
という名前にし、begin
でスタックに積み、end
でスタックから取り除くようにしています。また、dependentObservable
のevaluateImmediate
も循環で呼び出されないようにしましょう。begin
したらend
するのを忘れないようにしましょう。
TODO
をすべて埋めたらindex.htmlをブラウザで開いて、以下のテストプログラムの動作を確認してみましょう。
// Test
(function () {
var a = go.observable(100);
// 入れ子にしても問題ないか?
var b = go.computed(function () {
var c = go.computed(function () {
return a() * 2;
});
return c();
});
go.log(a(),b());
a(150);
go.log(a(),b());
})();
以下のように出力されているはずです。
- [100,200]
- [150,300]
b
の依存関係とb
のreadFunction
の中で作られているc
の依存関係はスタックを使うことで別物として扱うことができるようになりました。a
を変更すると、b
とc
に変更が通知され、それぞれのevaluateImmediate
が実行されます。もちろん、c
はb
のreadFunction
が呼ばれる度に生成されるので、普通はこのような呼び方はしないでしょう。
まとめ
KnockoutJSの以下のオブジェクトの簡易実装を作ってみました。KnockoutJSのデータバインドはdependenctyDetection
がキモで、この機構があるおかげで、明示的に依存関係を定義する必要がありません。しかしながら、その仕組みをあまり理解せずに使用していると、思わぬバグに遭遇して困るのではないでしょうか。ここでは、説明が複雑になるので、ko.applyBindings
を使ったHTMLとの双方向バインディングの仕組みについて触れませんでしたが、ソースを軽く見た感じではcomputed
をうまく使って実装されているようです。また時間があれば、そちらの実装の方もまとめたいと思います。これを書いている間もmasterブランチのソースコードが結構変わっているので、内容が古くなるかもしれませんが、このまとめが誰かの役に立てば幸いです。