内郚からの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-.

Source: https://habr.com/ru/post/J340592/


All Articles