内部からのmobxの動作とreduxとの比較





ロシア語を話す反応コミュニティのチャットを電報( https://t.me/react_js )で読むと、mobxの議論が一定の規則性で現れ、reduxとの比較で魔法、複雑さ、「可変性」についての議論があり、 mobxとは何か、どのmobxが解決するのかについての大きな誤解。 そして、すべての議論を1つの投稿で収集できるように、この記事を「報告」で書くことにしました。 独自のバージョンのmobxを実装することにより、mobxが内部からどのように機能するかを分析し、reduxの機能と比較します。



まず、mobxは、状態管理ライブラリとして他のライブラリと比較され@observable



デコレータ@observable



マークされたプロパティが変更@observable



後、reactのコンポーネントを呼び出すことを除いて、状態を操作するための利便性をほとんど提供しません。 すべての@observable



および@observer



デコレーターを削除し、動作中のアプリケーションを取得することで、 @observable



を簡単に破棄できます。コンポーネントに表示される状態データを変更するすべてのイベントハンドラーの最後にupdate()



を1行追加するだけです。







 onCommentChange(e){ const {comment} = this.props; comment.text = e.target.value; update(); //   }
      
      





そしてupdate()関数は単にreactアプリケーションの「再レンダラー」を呼び出し、reactの仮想運命のおかげで、実際の思考ではdiffの変更のみが適用されます







 function update(){ ReactDOM.render(<App>, document.getElementById('root'); }
      
      





mobxはハンドラー内のupdate()



1行を保存するので、ステートマネージャー全体であると言うのは、いくぶん多すぎます。







対照的に、reduxでは、状態を適切に更新せずに変更オブジェクト(アクション)を「ディスパッチ」し、完全に異なる場所で処理する場合(いわゆる純粋なレデューサー関数)で、イベントソーシングパターンを通じて状態で作業を整理できます。イベントバスに、ミドルウェアパイプラインでこれらのアクションをインターセプトし、タイムトラベル機能を介してデバッグアプリケーションを簡素化することにより、非同期の便利な作業を追加できます。

つまり、mobxは状態の処理を単純化するライブラリではありません。その主なタスクは何ですか? その主なタスクは、コンポーネントのポイントごとの更新です。つまり、変更されたデータに依存するコンポーネントのみを更新します。

上記の例では、アプリケーションのデータが変更されるReactDOM.render(<App>, document.getElementById('root'))



に、 update()



関数でReactDOM.render(<App>, document.getElementById('root'))



を呼び出して、アプリケーション全体の「再レンダラー」(仮想思考比較)を実行します。これはパフォーマンスに影響し、大規模なアプリケーションではインターフェースの速度が低下することは避けられません。







現実の運命は遅いというスローガンと、メモリ内のオブジェクトのツリーのみを比較するため仮想運命で反応する仮想運命を生み出したという事実にもかかわらず、実際の運命では変更された部分のみを更新しますが、実際にはアプリケーションのデータが更新されるたびにこれを呼び出すことはできません遅いため、アプリケーション全体の仮想の運命を比較します。



そして、問題の解決策は仮想運命に依存せず、コンポーネントを手動で更新し、出力するデータが変更されたコンポーネントのみのthis.forceUpdate()



呼び出します。

そして、この問題は、まさにmobxライブラリとreduxライブラリの一部が解決するものです。







しかし、これら2つのライブラリを考慮せずに、コンポーネントを個別に更新する問題を解決してみましょう。







ここでは、2つのアプローチを考え出すことができます。どちらも、州との連携方法に制限を課します。

最初のアプローチは不変性とバイナリ検索を使用することです-各状態の更新がすべての親オブジェクトに対して変更された新しいデータオブジェクトを返す場合(状態に階層構造がある場合)、リンクを前と新しいと比較することでコンポーネントのほぼポイントごとの更新を達成できますデータが変更されていないコンポーネント(newSubtree === oldSubtree)のすべてのサブツリーをステートスキップし、その結果、必要なcomのレンダラーを呼び出してアプリケーションを更新します。 構成要素の数である - onenta n個のデータのみO(ログ(N))の成分と比較。







したがって、たとえば、 ChangeDetectionStrategy.OnPush



設定すると角度は機能します。 しかし、上から下に下るという決定には、いくつかの欠点があります。 まず、O(log(n))の効率にもかかわらず、あるコンポーネントが他のコンポーネントのリストを表示する場合、コンポーネントの配列全体を調べて小道具を比較し、リストの各コンポーネントが別のリストをレンダリングする場合、その後、比較の数がさらに増加し​​ます。 第二に-コンポーネントは独自のプロップのみに依存する必要があり、プロップは多くの場合、中間コンポーネントを介してスローする必要があります。



reduxライブラリーも不変のアプローチを使用しますが、わずかに変更するだけで、小道具のみに依存するという欠点を解決します。 reduxは、小道具の比較に加えて、状態の異なる部分への依存を示すmapStateToProps()



関数( connect



デコレーター)によって返された追加データも比較し、それらは追加の小道具になります。 ただし、このため、接続のすべてのコンポーネントをチェックする必要があります。 ただし、これでもアプリケーション全体の更新( ReactDOM.render(<App>, rootEl);



)よりも高速です。

しかし、免疫的アプローチには、国家との協力に制限を課すいくつかの重大な欠点があります。







最初の欠点は、アプリケーション内のデータオブジェクトのプロパティを取得して更新することができないことです。 状態全体の新しい不変オブジェクトを毎回返す必要があるため、新しいオブジェクトを返し、すべての親オブジェクトと配列を再作成する必要があります。 たとえば、状態オブジェクトにプロジェクトの配列が格納されている場合、各プロジェクトにはタスクの配列が格納され、各タスクにはコメントの配列が格納されます。







 let AppState = { projects: [ {..}, {...}, {name: 'project3', tasts: [ {...}, {...}, {name: 'task3', comments: [ {...}, {...}, {text: 'comment3' } ]} ]} ] }
      
      





コメントオブジェクトのテキストを更新するために、 comment.text = 'new text'



実行することはできません-最初にコメントオブジェクトを再作成する必要があります( comment = {...comment, text: 'updated text'}



)、次にする必要がありますタスクオブジェクトを再作成し、そこから他のコメントへのリンクをコピーし( task = {...task, tasks: [...task.comments]}



)、プロジェクトオブジェクトを再作成し、そこに他のタスクへのリンクをコピーします( project = {...project, tasks: [...project.tasks]}



)そして最後に既に状態オブジェクトを再作成し、他のプロジェクトへのリンクもコピーします( AppStat = {...AppState, projects: [...AppState.projects]}



) 。







2番目の欠点は、ある状態で相互に参照するオブジェクトを保存できないことです。 コンポーネントハンドラーのどこかでタスクが配置されているプロジェクトを取得する必要がある場合、オブジェクトを作成するときに親プロジェクトtask.project = project



へのリンクを単純に割り当てることはできません。不変のアプローチの必要性はタスクだけでなく、プロジェクトのその他のすべてのタスクを更新する必要があるという事実につながります-プロジェクトオブジェクトへのリンクが変更されました。つまり、新しいリンクを割り当てることですべてのタスクを更新する必要があります。 オブジェクト、タスクがコメントを保存されている場合と、我々は、彼らが問題のオブジェクトへの参照が格納されるため、コメントのすべてを再作成するために必要な、と我々はに来るので、再帰的に全体の状態を再作成し、それがひどく遅くなります。



その結果、目的のオブジェクトを転送するたびに高次コンポーネントの小道具を変更するか、オブジェクトを参照する代わりにtask.project = '12345';



保存するtask.project = '12345';



そして、識別子によってプロジェクトのハッシュを保存および維持する場所ProjectHash['12345'] = project;









不変性を伴うソリューションには多くの欠点があるため、点ベースのコンポーネント更新の問題を別の方法で解決できるかどうかを考えてみましょう。 アプリケーションのデータを変更する場合、このデータに依存するコンポーネントのみを再レンダリングする必要があります。 依存とはどういう意味ですか? たとえば、コメントテキストを表示する簡単なコメントコンポーネントがあります







 class Comment extends React.Component { render(){ const {comment} = this.props; return <div>{comment.text}</div> } }
      
      





このコンポーネントはcomment.text



依存しており、 comment.text



が変更されるたびに更新する必要があります。 ただし、コンポーネントに<div>{comment.parent.text}</div>



が表示される場合でも、 .text



だけでなく.parent



.parent



たびにコンポーネントを更新する必要があり.parent



。 免疫アプローチを適用せずにこの問題を解決できますが、javascriptのgetterおよびsetterの機能を使用します。これは、ポイント更新uiの問題を解決するための2番目のアプローチです。







ゲッターとセッターは、プロパティを更新したりプロパティ値を取得したりするハンドラーを配置する、かなり古いjavascript機能です。

 Object.defineProperty(comment, 'text', { get(){ console.log('>text getter'); return this._text; }, set(val){ console.log('>text setter'); this._text = val; } }) comment.text; //    >text getter comment.text = 'new text' //    >text setter
      
      





そのため、新しい値が割り当てられるたびに実行される関数をセッターに配置し、このプロパティに依存するコンポーネントのリストのレンダラーを呼び出します。 どのコンポーネントがどのプロパティに依存しているかを調べるには、コンポーネントのrender()



関数の開始時に現在のコンポーネントをグローバル変数に割り当てる必要があります。オブジェクトのプロパティのgetterを呼び出すときは、グローバル変数にあるこのプロパティの依存関係リストに現在のコンポーネントを追加する必要があります また、コンポーネントはツリー状に「レンダリング」できるため、前のコンポーネントをこのグローバル変数に戻すことを忘れないでください。







 let CurrentComponent; class Comment extends React.Component { render(){ const prevComponent = CurrentComponent; CurrentComponent = this; const {comment} = this.props; var result = <div>{comment.text}</div> CurrentComponent = prevComponent; return result } } comment._components = []; Object.defineProperty(comment, 'text', { get(){ this._components.push(CurrentComponent); return this._text }, set(val){ this._text = val; this._components.forEach(component => component.setState({})) } })
      
      





私はあなたがアイデアを得ると思います。 このアプローチでは、各プロパティはその依存コンポーネントの配列を格納し、プロパティが変更されるとそれらが更新されます。







ここで、依存コンポーネントの配列のストレージをデータと混合せず、コードを簡素化するために、そのようなプロパティのロジックをCellクラスに取り出します。これは、類推からわかるように、Excelのセルの原理に非常に似ています-他のセルが現在の式に依存している場合セル、値を変更すると、すべての依存セルが更新されます。







 let CurrentObserver = null; class Cell { constructor(val){ this.value = val; this.reactions = new Set(); //        es6  } get(){ if(CurrentObserver){ this.reactions.add(CurrentObserver); } return this.value; } set(val){ this.value = val; for(const reaction of this.reactions){ reaction.run(); } } unsibscribe(reaction){ this.reactions.delete(reaction); } }
      
      





ただし、式を持つセルの役割は、 Cell



クラスから継承するComputedCell



クラスによって果たされます(他のセルはこのセルに依存する可能性があるため)。 ComputedCell



クラスは、コンストラクターで再計算のための関数(式)と、副作用( .forceUpdate()



コンポーネントの呼び出しなど)を実行するためのオプションの関数を.forceUpdate()



ます







 class ComputedCell extends Cell { constructor(computedFn, reactionFn, ){ super(undefined); this.computedFn = computedFn; this.reactionFn = reactionFn; } run(){ const prevObserver = CurrentObserver; CurrentObserver = this; const newValue = this.computedFn(); if(newValue !== this.value){ this.value = newValue; CurrentObserver = null; this.reactionFn(); this.reactions.forEach(r=>r.run()); } CurrentObserver = prevObserver; } }
      
      





そして、ゲッターとセッターを毎回実行しないように、typescriptまたはbabelのデコレーターを使用します。 はい、これはクラスを使用し、リテラルconst newComment = {text: 'comment1'}



なくconst comment = new Comment('comment1')



を介してオブジェクトを作成する必要性に制限を課しますが、ゲッターとセッターを手動で設定する代わりに、プロパティを便利にマークできます@observable



通常のプロパティとしてそれ@observable



引き続き使用する方法。







 class Comment { @observable text; constructor(text){ this.text = text; } } function observable(target, key, descriptor){ descriptor.get = function(){ if(!this.__observables) this.__observables = {}; const observable = this.__observables[key]; if (!observable) this.__observables[key] = new Observable() return observable.get(); } descriptor.set = function(val){ if (!this.__observables) this.__observables = {}; const observable = this.__observables[key]; if (!observable) this.__observables[key] = new Observable() observable.set(val); } return descriptor }
      
      





また、コンポーネント内のComputedCell



クラスを直接操作しないように、このコードを@observer



デコレーターに配置します。これは、 render()



メソッドをラップし、最初の呼び出しで計算セルを作成し、 render()



メソッドを式および関数として渡しますthis.forceUpdate()



this.forceUpdate()



呼び出しthis.forceUpdate()



実際には、 componentWillUnmount()



メソッドにサブスクthis.forceUpdate()



解除を追加し、リアクションのコンポーネントの正しいラッピングの瞬間を追加する必要がありますが、ここでは理解を容易にするためにこのオプションを残します)







 function observer(Component) { const oldRender = Component.prototype.render; Component.prototype.render = function(){ if (!this._reaction) this._reaction = new ComputedCell(oldRender.bind(this), ()=>this.forceUpdate()); return this._reaction.get(); } }
      
      





として使用します







 @observer class Comment extends React.Component { render(){ const {comment} = this.props; return <div>{comment.text}</div> } }
      
      





デモへのリンク







すべてのコードアセンブリ
 import React from 'react'; import { render } from 'react-dom'; let CurrentObserver; class Cell { constructor(val) { this.value = val; this.reactions = new Set(); } get() { if (CurrentObserver) { this.reactions.add(CurrentObserver); } return this.value; } set(val) { this.value = val; for (const reaction of this.reactions) { reaction.run(); } } unsubscribe(reaction) { this.reactions.delete(reaction); } } class ComputedCell extends Cell { constructor(computedFn, reactionFn) { super(); this.computedFn = computedFn; this.reactionFn = reactionFn; this.value = this.track(); } track(){ const prevObserver = CurrentObserver; CurrentObserver = this; const newValue = this.computedFn(); CurrentObserver = prevObserver; return newValue; } run() { const newValue = this.track(); if (newValue !== this.value) { this.value = newValue; CurrentObserver = null; this.reactionFn(); } } } function observable(target, key) { return { get() { if (!this.__observables) this.__observables = {}; let observable = this.__observables[key]; if (!observable) observable = this.__observables[key] = new Cell(); return observable.get(); }, set(val) { if (!this.__observables) this.__observables = {}; let observable = this.__observables[key]; if (!observable) observable = this.__observables[key] = new Cell(); observable.set(val); } } } function observer(Component) { const oldRender = Component.prototype.render; Component.prototype.render = function(){ if (!this._reaction) this._reaction = new ComputedCell(oldRender.bind(this), ()=>this.forceUpdate()); return this._reaction.get(); } } class Timer { @observable count; constructor(text) { this.count = 0; } } const AppState = new Timer(); @observer class App extends React.Component { onClick=()=>{ this.props.timer.count++ } render(){ console.log('render'); const {timer} = this.props; return ( <div> <div>{timer.count}</div> <button onClick={this.onClick}>click</button> </div> ) } } render(<App timer={AppState}/>, document.getElementById('root'));
      
      





この例では、1つの欠点があります。コンポーネントの依存関係が変更される可能性がある場合はどうでしょうか。 次のコンポーネントをご覧ください







 class User extends React.Component { render(){ const {user} = this.props; return <div>{user.showFirstName ? user.firstName : user.lastName}</div> } }
      
      





コンポーネントはuser.showFirstName



プロパティに依存し、さらに値に応じて、 user.firstName



またはuser.lastName



いずれかに依存できます。つまり、 user.showFirstName == true



場合、 user.showFirstName == true



変更に反応するべきではありません。 user.showFirstName



falseに変更されたため、プロパティuser.firstName



場合、反応(およびコンポーネントレンダラーを実行)しないでuser.firstName









この点は、依存関係リストthis.dependencies = new Set()



をセルクラスに追加し、 run()



関数の小さなロジックを追加することで簡単に解決できます。そのため、反応のrender()を呼び出した後、以前の依存関係のリストを新しいものと比較し、無関係な依存関係からサブスクライブを解除します。







 class Cell { constructor(){ ... this.dependencies = new Set(); } get() { if (CurrentObserver) { this.reactions.add(CurrentObserver); CurrentObserver.dependencies.add(this); } return this.value; } } class ComputedCell { track(){ const prevObserver = CurrentObserver; CurrentObserver = this; const oldDependencies = this.dependencies; //    this.dependencies = new Set(); //          const newValue = this.computedFn(); //        for(const dependency of oldDependencies){ if(!this.dependencies.has(dependency)){ dependency.unsubscribe(this); } } CurrentObserver = prevObserver; return newValue; } }
      
      





2番目のポイントは、オブジェクトの多くのプロパティをすぐに変更した場合ですか? 依存コンポーネントは同期的に更新されるため、2つの追加コンポーネント更新が取得されます







 comment.text = 'edited text'; //    comment.editedCount+=1; //   
      
      





不要な更新を避けるために、この関数の先頭でグローバルフラグを設定できます。 @observer



デコレーターはすぐにthis.forceUpdate()



呼び出さず、このフラグを削除したときにのみ呼び出します。 そして簡単にするために、このロジックをaction



デコレータに配置し、フラグの代わりにデコレータを他のデコレータ内で呼び出すことができるため、カウンタを増減します。







 updatedComment = action(()=>{ comment.text = 'edited text'; comment.editedCount+=1; }) let TransactionCount = 0; let PendingComponents = new Set(); function observer(Component) { const oldRender = Component.prototype.render; Component.prototype.render = function(){ if (!this._reaction) this._reaction = new ComputedCell(oldRender.bind(this), ()=>{ TransactionCount ?PendingComponents.add(this) : this.forceUpdate() }); return this._reaction.get(); } } function action(fn){ TransactionCount++ const result = fn(); TransactionCount-- if(TransactionCount == 0){ for(const component of PendingComponents){ component.forceUpdate(); } } return result; }
      
      





その結果、非常に古い「オブザーバー」パターンを使用するこのアプローチ(オブザーバブルRxJSと混同しないでください)は、イミュニティアプローチよりもポイント更新コンポーネントのタスクにより適しています。







欠点のうち、リテラルではなくクラスを介してオブジェクトを作成する必要があることに気付くことができます。つまり、サーバーから一部のデータを@observable



てコンポーネントに渡すことはできません。クラスオブジェクトを@observable



デコレータでラップして追加のデータ処理を実行する必要があります。







欠点には、オブジェクトに新しいプロパティをオンザフライで追加できないこと(jsパフォーマンスの観点から既にアンチパターンと見なされています)、データがゲッターの背後に隠され、値の代わりに3つのドットが表示されるため、クロムdevtoolsのコードデバッグの不便さが含まれますこのプロパティをクリックする値、および変更をステップスルーするか、プロパティを取得しようとすると、ライブラリ内のセッターまたはゲッターに深く入ります。



しかし、利点は欠点をはるかに超えています。 まず、不変のアプローチとは異なり、更新の必要なコンポーネントのリストがすぐにわかるため、作業の速度はコンポーネントの数に依存しません-つまり、o(log(n))またはo(n)の代わりにo(1)の複雑さがありますDen Abramov、さらに重要なことに、nオブジェクトはmapStateToProps



関数では作成されません。 次に、一部のデータを更新する必要がある場合、 comment.text = 'new text'



と記述するだけで、親の状態オブジェクトを更新するために多くの作業を行う必要はありません。重要なことは、ガベージコレクターに負荷がかからないことです。オブジェクトの永続的なレクリエーション。 最も重要なこと-相互に参照するオブジェクトを使用して状態をモデル化でき、オブジェクトの代わりに識別子を保存することなく状態を操作でき、 AppState.folders[AppState.projects[AppState.tasks[comment.taskId].projectId].folderId].name



だけでAppState.folders[AppState.projects[AppState.tasks[comment.taskId].projectId].folderId].name



クリックする代わりに



おわりに



— — "" mobx. mobx @computed



( ) mobx- react-.








All Articles