コントローラのライフサイクルについて
- 概要
- コールバックによる通知
- 非同期処理と実行順序
- 動的に子コントローラが追加される場合の実行順序
- 動的に子コントローラが外される場合の実行順序
- manageChildされるコントローラのルートエレメントについて
- ライフサイクル処理実装時の注意
- エラー時のコントローラのdispose処理
- lifecycleerrorイベント
概要
コントローラには、生成~動作開始~破棄のライフサイクルがあります。
hifiveのコントローラは、コールバック・イベント・Promiseオブジェクトを通して、
ライフサイクルの各タイミングを通知します。
ライフサイクルは、以下8つのタイミングが定義されています。
ただし、一部のタイミングは、使用される状況を考慮し、特定の方式でのみ検知可能としています。
- construct
- preInit
- init
- postInit
- h5controllerbound
- ready
- h5controllerready
- unbind
- h5controllerunbound
- dispose
コールバックによる通知
コントローラが自分自身のライフサイクルタイミングを知る場合はコールバックを使用します。
__construct, __init, __postInit, __ready, __unbind, __dispose の5つが定義されています。
これらの名前でコールバック関数を定義しておくと、それぞれのタイミングで関数が自動的に呼ばれます。
例:
__name: 'SampleController',
__ready: function(context) {
//コントローラの初期化処理が完了し、動作を開始したタイミングで呼ばれる
}
};
生成~初期化時のライフサイクル
コントローラの生成時に呼ばれるライフサイクルイベント(__construct, __init, __postInit, __ready)は
- __construct
- __init
- __postInit
- __ready
の順に実行されます。
また、各コールバック内の処理を非同期化したい場合は、そのコールバックでPromiseオブジェクトを返すようにしてください。
Promiseが返された場合、フレームワークは、そのPromiseが完了するまで、初期化処理を遅延させます。
(※ただし、__constructに限ってはPromiseを返しても同期的に実行します)
__construct
__constructはコントローラ化(インスタンス化)が終わった段階で呼ばれます。
__constructは親コントローラから子コントローラへと再帰的に実行されます。
実行までに行われる処理
- コントローラ化
未実行の処理
- テンプレートのロード
- rootElementのセット(コントローラの要素へのバインド)
- rootControllerとparentControllerのセット
- イベントハンドラのバインド
__init
__initはコントローラ化とテンプレートのロード、コントローラの要素へのバインドが終わった段階で呼ばれます。
__initは親コントローラから子コントローラへと再帰的に実行されます。
実行までに行われる処理
- コントローラ化
- テンプレートのロード
- rootElementのセット(コントローラの要素へのバインド)
- rootControllerとparentControllerのセット
未実行の処理
- イベントハンドラのバインド
__postInit
__postInitは全てのコントローラの__initが終わった段階で呼ばれます。この段階では全てのコントローラについてテンプレートのロード、rootElementのセット、rootControllerとparentControllerのセットが完了しています。
__postInitは子コントローラから親コントローラへと再帰的に実行されます。
実行までに行われる処理
- コントローラ化
- テンプレートのロード
- rootElementのセット(コントローラの要素へのバインド)
- rootControllerとparentControllerのセット
未実行の処理
- イベントハンドラのバインド
__ready
__readyは全ての処理が終わった段階で呼ばれます。
__readyは子コントローラから親コントローラへと再帰的に実行されます。
実行までに行われる処理
- コントローラ化
- テンプレートのロード
- rootElementのセット(コントローラの要素へのバインド)
- rootControllerとparentControllerのセット
- イベントハンドラのバインド
コントローラのアンバインド~破棄のライフサイクル
バインドしたコントローラは、任意のタイミングでアンバインドしたり、完全に破棄したりできます。
- __unbind
- __dispose
のライフサイクルが定義されており、それぞれアンバインドと破棄のタイミングに対応しています。
__unbind
コントローラ化を行ったときに追加されるメソッドunbind()を実行すると、__unbindが呼ばれます。
unbind()は、(現在バインドされている)DOM要素からイベントハンドラを削除します。
この処理は、コントローラ化時にエラーが発生した場合または、ユーザがコントローラのメソッドdispose()またはunbind()を呼びだした場合に実行されます。
__unbindは子コントローラから親コントローラへと再帰的に実行されます。
__dispose
コントローラ化したオブジェクトが破棄されるタイミングで呼ばれます。
この処理は、コントローラ化時にエラーが発生した場合または、ユーザがコントローラのメソッドdispose()を呼びだした場合に実行されます。
dispose()は、アンバインド処理(unbind())を行った後、オブジェクトの全プロパティにnullを代入してオブジェクトを開放します。
従って、一度dispose()されたコントローラ化済みオブジェクトは再利用できません。
__disposeハンドラがPromiseを返さなかった場合、破棄処理は即時実行されます。
__disposeハンドラがPromiseを返した場合、そのPromiseが成功、失敗に関わらず、完了した後にプロパティへのnull代入処理が行われます。
__disposeは子コントローラから親コントローラへと再帰的に実行されます。
非同期処理と実行順序
__init, __postInit, __readyで非同期処理の実行と実行順の制御を両立させたい場合、
それぞれのイベントでPromiseオブジェクトを戻り値として戻すことによって実現できます。
Promiseオブジェクトについてはこちらを参照してください。
__constructの非同期処理はサポートしていません。
__name: 'SampleController',
__construct: function(context) {
var param = context.args;
this.log.info(param.construct); // "construct"とコンソールに表示
},
__init: function(context) {
var param = context.args;
var dfd = this.deferred();
setTimeout(this.own(function() {
this.log.info(param.init); // 300ms後に"init"とコンソールに表示
dfd.resolve();
}), 300);
return dfd.promise();
},
__ready: function(context) {
var param = context.args;
this.log.info(param.ready); // __"ready"とコンソールに表示
}
};
h5.core.controller(element, controller, {
construct: 'construct',
init: 'init',
ready: 'ready'
});
動的に子コントローラが追加される場合の実行順序
manageChild()によって、子コントローラを動的に追加することができます。その時、親コントローラ及び子コントローラのライフサイクルイベントが全て完了している場合は再度ライフサイクルイベントが実行されることはありません。
しかし、ライフサイクルイベントがまだ完了していないコントローラがmanageChildをする、あるいはmanageChildされる場合は、ライフサイクルイベントの実行順序はmanageChildを呼んだ時点でライフサイクルイベント実行順序の同期をとり、親子間の順番が守られます。
以下、具体例を交えて説明します。
manageChild()を使った動的子コントローラではなく、定義オブジェクトに子コントローラが定義されているコントローラ(静的子コントローラを持つコントローラ)のライフサイクル実行順序は以下のようになります。
var bControllerDef = {
__name: 'BController',
/* 省略 */
};
var aControllerDef = {
__name: 'AController',
childController: bControllerDef,
/* 省略 */
};
h5.core.controller(document.body, aControllerDef);
AControllerとBControllerを独立してバインドした(それぞれh5.core.controller()が呼んだ)後、AControllerがBControllerをmanageChild()した場合を考えます。
var bController = h5.core.controller($bTarget, bControllerDef);
// 以下をどのタイミングで実行するかによって、ライフサイクルイベントの順序が変わる
// aController.manageChild(bController);
manageChild()による子コントローラ(動的子コントローラ)の場合も、基本的には静的子コントローラの場合と同じ順序で実行します。例えば、両コントローラの__constructが終わった直後、__initが実行される前にバインドした場合のライフサイクルイベント実行順序は以下のようになります。
var bController = h5.core.controller($bTarget, bControllerDef);
// aもbも__constructが終わった直後のタイミング
aController.manageChild(bController);
両コントローラが同じライフサイクルイベントの実行まで進んだタイミングでのmanageChild()は、そこからのライフサイクル実行順序は静的子コントローラの場合と同じです。
- 例:お互いに__initまで呼ばれたタイミングでmanageChild
__name: 'BController',
/* 省略 */
};
var bController = h5.core.controller(documentBody, bControllerDef);
var aContrllerDef = {
__name: 'AController',
__init: function(){
// __initの呼び出しが終わっているタイミング
bController.initPromise.done(this.own(function(){
this.manageChild(bController);
});
}
/* 省略 */
};
var aController = h5.core.controller(document.body, aControllerDef);
- 例:お互いに__readyまで呼ばれたタイミングでmanageChild
__name: 'BController',
/* 省略 */
};
var aContrllerDef = {
__name: 'AController',
__ready: function(){
// a,b両方の__readyの呼び出しが終わっているタイミング
var bReadyPromise = bController.readyPromise;
bReadyPromise.done(this.own(function(){
this.manageChild(bController);
}));
// promiseを返してmanageChildが完了するまで待機
return bReadyPromise;
}
/* 省略 */
};
h5.core.controller(document.body, aControllerDef);
manageChild()されたコントローラはルートコントローラではなくなります。そのため、manageChild()されたコントローラがまだh5controllerboundまたはh5controllerreadyのイベントをあげていなかった場合は、それらのイベントはmanageChild()されたコントローラからはあがらなくなります。
次に、manageChild()する側あるいはされる側のコントローラのライフサイクルが、もう片方と比べて進んでいたり遅れている場合を見てみましょう。
この場合は、片方と比べてライフサイクルの実行が進んでいる側のコントローラが、遅れている側のコントローラのライフサイクルの実行を待機してから、残りのライフサイクルイベントを静的子コントローラの場合と同様の順序で実行します。
- 例:manageChild()する側のコントローラが__postInitが呼ばれたタイミング、される側のコントローラがまだ__initが呼ばれていない(__construct直後)の場合
__name: 'AController',
__postInit: function(){
var bController = h5.core.controller(document.body, bControllerDef);
this.manageChild(bController);
}
};
var bControllerDef = {
__name: 'BController'
};
h5.core.controller(document.body, aControllerDef);
動的に子コントローラが外される場合の実行順序
ライフサイクルイベントの実行途中のタイミングで、unmanageChild()によって子コントローラを動的に外した場合、外されたコントローラのライフサイクルと外した側のコントローラのライフサイクルの実行は切り離されます。
unmageChild()は、第2引数にtrueを指定すると第1引数で指定したコントローラをdisposeします(デフォルト)。明示的にfalseを指定した場合は第1引数のコントローラをdisposeせず、その子コントローラをルートコントローラとして扱います。
__name: 'BController'
};
var aControllerDef = {
__name: 'AController'
childController: bControllerDef
};
aController = h5.core.controller(document.body, aControllerDef);
aController.initPromise.done(function(){
// andDisposeがfalseならdisposeされる。trueならdisposeしないでルートコントローラとしてバインド
this.unmanageChild(this.childController, andDispose);
});
- andDisposeがtrue(デフォルト)の場合のライフサイクル
- andDisposeがfalseの場合のライフサイクル
ルートエレメントの決定していない子コントローラを第2引数にfalseを指定してunmanageChild()することはできません。例えば、静的子コントローラのルートエレメントは親コントローラの__init直後に決まりますが、それより前のタイミングで静的子コントローラをunmanageChild()することはできません。
__name: 'AController',
childController: bControllerDef,
__init: function(){
// 以下はエラー。childControllerのルートエレメントはこのタイミングでは未決定のため。
// this.unmanageChild(this.childController, false);
// 以下は実行可能
this.unmanageChild(this.childController, true);
}
}
manageChildされるコントローラのルートエレメントについて
コントローラのライフサイクルでは、親コントローラ__init実行後に子コントローラのルートエレメントが決定します。
ただし、その子コントローラがmanageChild()によって動的に追加された子コントローラの場合は、ルートエレメントの決定は行わず、manageChild()される前のコントローラのルートエレメントになります。なぜなら、manageChild()の対象となるコントローラはルートコントローラであり、ルートエレメントは既に決定済みであるためです。manageChild()によってその決定済みのルートエレメントは変更しません。
つまり、どのタイミングでmanageChild()を行ったとしても、それによるルートエレメントの変更はありません。
ライフサイクル処理実装時の注意
ライフサイクル関数の中では、基本的に、例外を外部にスローしないでください。
もし例外を送出した場合は、コントローラはdisposeされます。
また、ライフサイクルイベントが返したPromiseが失敗した場合も、コントローラはdisposeされます。
エラー時のコントローラのdispose処理
コントローラのライフサイクルで例外が投げられた場合、またはライフサイクルが返したPromiseオブジェクトが失敗した場合、コントローラはdisposeされます。正常時のdispose処理(dispose()メソッドを呼んでdisposeした場合)とは、以下の点で異なります。
- コントローラのプロパティへのnull代入は行われない。
- lifecycleerrorイベント(後述)が上がる。
- 例外が起きた場合は、dispose処理が全て終わった後にエラーが投げられる。
エラー時のdispose処理でも、正常時と同様に、__unbind、__disposeのライフサイクルは呼ばれます。__unbind、__disposeの呼び出しの途中で例外が投げられた場合も、__unbind、__disposeの呼び出しは中断しません。この時に複数のライフサイクルイベントで例外が起きた場合に、は最初に発生したエラーが投げられます。
__name: 'controller',
__init: function(){
throw new Error('__init error');
// エラーが発生したのでdispose処理が実行される。初期化処理は中断される。
},
__postInit:function(){
// 初期化処理は中断されたのでここは実行されない。
},
__ready(){
// __postInitと同様実行されない
},
__unbind: function(){
// 実行される。異常終了時のdisposeも正常時と同様に、__unbind,__disposeは実行される
throw new Error('__unbind error');
// 最終的に投げられる例外は既に決まっている(__initの例外==最初に発生した例外)なので
// この例外はFWが飲む(外に投げられない)
},
__dispose: function(){
// 実行される。__unbindでエラーが投げられてもdispose処理は継続する
throw new Error('__dispose error');
// 最終的に投げられる例外は既に決まっているので、この例外はFWが飲む(外に投げられない)
}
};
h5.core.controller(element, controller);
window.onerror = function(e){
// 例外:'__init error'が発生する。
// __initは非同期で実行されるので、__initで投げている例外も非同期で投げられる。
// そのため、h5.core.controller()をtry-catchで囲っても拾えないことに注意。
// (__constructで例外を投げた場合はtry-catchで拾える)
}
lifecycleerrorイベント
コントローラが異常終了してdisposeされた時は、h5.core.controllerManagerにlifecycleerrorイベントが上がります。コントローラの異常終了を監視するには以下のようにしてlifecycleerrorイベントハンドラを記述します。
// コントローラが異常終了された時に呼ばれるイベントハンドラ
// disposeされたコントローラ(ルートコントローラ)の取得
var rootController = ev.rootController;
// disposeされた原因(promiseが失敗した理由または、例外オブジェクト)を取得
var detail = ev.detail;
});
lifecycleエラーイベントは、ライフサイクルでの例外送出またはプロミスの失敗によるライフサイクルの失敗が原因でコントローラがdisposeされた(異常終了)時に、上がるイベントです。dispose()を呼んで正常にdisposeした場合はこのイベントは上がりません。