オブザーバーパターンは、OOP自体の出現以来、おそらく知られています。 リスナーのリストを保存し、「追加」、「削除」、および「通知」メソッドを持つオブジェクトがあり、外部コードがサブスクライブされるか、サブスクライバーに通知することを想像するのは簡単です
class Observable { listeners = new Set(); subscribe(listener){ this.listeners.add(listener) } unsubscribe(listener){ this.listeners.delete(listener) } notify(){ for(const listener of this.listeners){ listener(); } } }
reduxでは、このパターンは変更なしで適用されます-「react-redux」パッケージは、コンポーネントをラップするconnect
機能を提供し、componentDidMountが呼び出されると、 Observable
subscribe()
メソッドを呼び出し、 componentWillUnmount()
呼び出されると、 unsubscrib()
とdispatch()
を呼び出しますループ内ですべてのリスナーを呼び出すtrigger()
メソッド。それぞれが順番にmapStateToProps()
を呼び出し、値が変更されたかどうかに応じて、コンポーネント自体でsetState()
を呼び出します。 すべてが非常に単純ですが、実装のこのような単純さに対する支払いは、状態を不変に処理してデータを正規化する必要があり、単一のオブジェクトまたは1つのプロパティが変更された場合、その変更された状態の部分に依存せず、コンポーネント内で同時にすべてのコンポーネントサブスクライバーに通知しますサブスクライバーは、 mapStateToProps()
依存するストアのどの部分を明示的に示す必要があります
Mobxはこのパターンを使用する点でreduxに非常に似ています。オブザーバーはさらにパターンを開発するだけですmapStateToProps()
記述しない場合、コンポーネントは個別に「レンダリング」するデータに依存します。 アプリケーション全体の1つの状態オブジェクトでサブスクライバーを収集する代わりに、サブスクライバーは状態の各フィールドにサブスクライブします。 firstName
フィールドとlastName
フィールドを持つユーザーの場合、 firstName
とlastName
別々にredux-stor全体を作成するようです。
したがって、このような「ストア」を作成してサブスクライブする簡単な方法を見つけた場合、 mapStateToProps()
は必要ありません。これは、状態のさまざまな部分への依存がさまざまなストーリーの存在ですでに表現されているためです。
したがって、各フィールドには、別個の「ミニストア」があります。オブザーバーオブジェクトで、 subscribe()
、 unsubscribe()
、 trigger()
に加えて、 set()
を呼び出すときにget()
およびset()
メソッドと同様にvalue
フィールドが追加されますサブスクライバーは、値自体が変更された場合にのみ呼び出されます。
class Observable { listeners = new Set(); constructor(value){ this.value = value } get(){ return this.value; } set(newValue){ if(newValue !== this.value){ this.notify(); } } subscribe(listener){ this.listeners.add(listener) } unsubscribe(listener){ this.listeners.delete(listener) } notify(){ for(const listener of this.listeners){ listener(); } } } const user = { fistName: new Observable("x"), lastName: new Observable("y"), age: new Observable(0) } const listener = ()=>console.log("new firstName"); user.firstName.subscribe(listener) user.firstName.get() user.firstName.set("new name"); user.firstName.unsubscribe(listener);
同時に、ストアのイミュニティの要件は少し異なるように解釈する必要があります-各個別のストアにプリミティブ値のみを格納する場合、reduxの観点からuser.firstName.set("NewName")
呼び出しに問題はありません。これは不変の値です-ここでは、reduxのように、ストアの新しい不変の値を設定しています。 オブジェクトまたは複雑な構造を「ミニストア」に保存する必要がある場合は、それらを別々の「ミニストア」に入れるだけです。 代わりに例えば
const user = { profile: new Observable({email: "...", address: "..."}) }
コンポーネントが個別に"email"
または"address"
いずれかに依存し、不要な「再レンダリング」が発生しないように記述する方がよい
const user = { profile: { email: new Observable("..."), address: new Observable("..."} } }
2番目のポイント-このアプローチでは、プロパティへのアクセスごとにget()
メソッドを呼び出さなければならないことに気付くことができます。これは不便です。
const App = ({user})=>( <div>{user.firstName.get()} {user.lastName.get()}</div> )
しかし、この問題はjavascriptのゲッターとセッターによって解決されます
class User { _firstName = new Observable(""); get firstName(){ return this._firstName } set firstName(val){ this._firstName = val } }
また、デコレータに対して否定的な態度を持たない場合は、この例をさらに簡略化できます。
class User { @observable firstName = ""; }
一般的には、今のところ要約して、1)現時点では魔法はありません-デコレータはゲッターとセッターに過ぎない2)ゲッターとセッターは「ミニストア」のルート状態を読み取り、設定するだけです
さらに先に進みます-これをすべてリアクションに接続するには、コンポーネントで表示されるフィールドをサブスクライブしてから、componentWillUnmountでサブスクライブを解除する必要がありcomponentWillUnmount
this.listener = ()=>this.setState({}) componentDidMount(){ someState.field1.subscribe(this.listener) .... someState.field10.subscribe(this.listener) } componentWillUnmount(){ someState.field1.unsubscribe(this.listener) .... someState.field10.unsubscribe(this.listener) }
はい、コンポーネントに表示されるフィールドの増加に伴い、定規の数は何度も増加しますが、わずかな動きで数行のコードを追加することで手動サブスクリプションを完全に削除できます.get()
メソッドは.get()
方法でテンプレートで呼び出されるため、使用できますこれは自動サブスクリプションを作成します-コンポーネントのrender()
メソッドを呼び出す前に現在の配列をグローバル変数に書き込み、 .get()
メソッドでthis
をこの配列に追加し、呼び出しの最後に render()
メソッドの場合、現在のコンポーネントがサブスクライブされているすべての「ミニストア」の配列を取得します。 この単純なメカニズムは、コンポーネントがサブスクライブされる側がレンダリング中に動的に変化する状況、たとえばコンポーネントが<div>{user.firstName.get().length < 5 ? user.firstName.get() : user.lastName.get()}<div>
レンダリングするときなども解決し<div>{user.firstName.get().length < 5 ? user.firstName.get() : user.lastName.get()}<div>
<div>{user.firstName.get().length < 5 ? user.firstName.get() : user.lastName.get()}<div>
(名前の長さが5未満の場合、コンポーネントは名前の変更に応答せず(つまり、署名されません)、名前の長さが次の場合にサブスクリプションが自動的に行われます) 5)
let CurrentObservables = null; class Observable { listeners = new Set(); constructor(value){ this.value = value } get(){ if(CurrentObservables) CurrentObservables.add(this); return this.value; } set(newValue){ if(newValue !== this.value){ this.notify(); } } subscribe(listener){ this.listeners.add(listener) } unsubscribe(listener){ this.listeners.delete(listener) } notify(){ for(const listener of this.listeners){ listener(); } } } function connect(target){ return class extends (React.Component.isPrototypeOf(target) ? target : React.Component) { stores = new Set(); listener = ()=> this.setState({}) render(){ this.stores.forEach(store=>store.unsubscribe(this.listener)); this.stores.clear(); const prevObservables = CurrentObservables; CurrentObservables = this.stores; cosnt rendered = React.Component.isPrototypeOf(target) ? super.render() : target(this.props); this.stores = CurrentObservables; CurrentObservables = prevObservables; this.stores.forEach(store=>store.subscribe(this.listener)); return rendered; } componentWillUnmount(){ this.stores.forEach(store=>store.unsubscribe(this.listener)); } } }
ここで、 connect
関数は、リアクションのコンポーネントまたはステートレスコンポーネント(関数)をラップし、この自動サブスクリプションメカニズムのおかげで必要な「ミニストア」にサブスクライブするコンポーネントを返します。
その結果、必要なデータと通知がこのデータが変更された場合にのみ、そのような自動サブスクリプションメカニズムを取得しました。 コンポーネントは、サブスクライブ先の「ミニストア」のみが変更された場合にのみ更新されます。 これらの「ミニストア」が数千ある可能性のある実際のアプリケーションでは、この複数のストアメカニズムを使用して、1つのフィールドを変更すると、このフィールドのサブスクライバーの配列にあるコンポーネントのみが更新されますが、これらすべてに署名する場合はreduxアプローチを使用します片側に数千のコンポーネントがあり、変更のたびに、これらの数千のコンポーネントすべてをサイクルで通知する必要があります(同時に、 mapStateToProps
内のコンポーネントが依存する状態の部分をプログラマーに手動で強制的に記述させます)
さらに、この自動サブスクリプションメカニズムは、reduxだけでなく、関数メモ化などのパターンを改善し、reselectライブラリを置き換えることができます-createSelector()で関数が依存する関数を明示的に指定する代わりに、関数の上記のように依存関係が自動的に決定されますレンダー()
おわりに
Mobxは、「ポイント」コンポーネントの更新と関数のメモ化の問題を解決するオブザーバーパターンの論理的な開発です。 少しリファクタリングし、 Observable
のコンポーネントから上記の例のコードを抽出し、 .get()
および.set()
put getterおよびsetterを呼び出す代わりに、 .set()
のobservable
およびcomputed
デコレーターをほぼ取得します。 ほとんど-mobxには、ループ内の単純な呼び出しではなく、より複雑なサブスクライバー呼び出しアルゴリズムがあり、ダイヤモンド型の依存関係に対する不要なcomputed
呼び出しを排除しますが、これについては次の記事で詳しく説明します。
upd: 続きの記事