少し前まで、Reactは自身を「MVCのV」として位置付けていました。 このコミット後、マーケティングテキストは変更されましたが、本質は同じままです。Reactが表示を担当し、開発者が他のすべてを担当します。つまり、MVCの観点から言えば、ModelとControllerを担当します。
アプリケーションのモデル(状態)を管理するためのソリューションの1つはReduxです。 その外観は、MVCが処理できないフロントエンドアプリケーションの複雑さの増加に動機付けられています。
チーフテクニカルインペラティブソフトウェア開発-複雑性管理
- 完全なコード
Reduxは、予測可能な状態変化で複雑さを管理することを提案しています。 予測可能性は、 3つの基本原則によって達成されます。
- アプリケーション全体の状態が1か所に保存されます
- 状態を変更する唯一の方法は、アクションを送信することです
- すべての変更は純粋な関数を使用して行われます
Reduxは増加する複雑さを克服でき、戦うべきものはありましたか?
MVCはスケーリングしません
Reduxは、FacebookソリューションであるFluxに触発されています。 Facebook開発者 ( ビデオ )によると、Fluxを作成した理由は、MVCアーキテクチャパターンのスケーラビリティの問題でした。
Facebookによると、MVCを使用した大規模プロジェクトでのオブジェクトの関係は予測不能になります。
- modelOneはviewOneを変更します
- viewOneは変更中にmodelTwoを変更します
- modelTwoは変更中にmodelThreeを変更します
- modelThreeは、変更中にviewTwoとviewFourを変更します
MVCの変更の予測不可能性の問題は、Reduxの動機にも書かれています。 以下の図は、Facebook開発者がこの問題をどのように見ているかを示しています。
Fluxは、説明されているMVCとは対照的に、理解可能で細身のモデルを提供します。
- スポーンアクションを表示
- アクションがDispatcherに入る
- ディスパッチャー更新ストア
- 更新されたストアは変更のビューを通知します
- 再描画を表示
さらに、Fluxを使用して、いくつかのビューは関心のあるストアをサブスクライブし、これらのストアで何かが変更されたときにのみ更新できます。 このアプローチにより、依存関係の数が減り、開発が簡素化されます。
FacebookのMVC実装は、Smalltalkの世界で広く配布されていた元のMVCとはまったく異なります。 この違いが、「MVCはスケーリングしない」というステートメントの主な理由です。
80年代に戻る
MVCは、Smalltalk-80でユーザーインターフェイスを開発するための主なアプローチです。 FluxやReduxと同様に、MVCはソフトウェアの複雑さを軽減し、開発を迅速化するために作成されました。 MVCアプローチの基本原理について簡単に説明します 。より詳細な概要については、 こちらとこちらをご覧ください 。
MVCエンティティの責任:
- モデルは、実世界とビジネスロジックをモデル化し、その状態に関する情報を提供し、Controller'aからの要求に応じて状態を変更する中央エンティティです。
- Viewはモデルステータス情報を受け取り、ユーザーに表示します。
- コントローラーはマウスの動きを監視し、マウスとキーボードのボタンをクリックして、ビューまたはモデルを変更して処理します
そして今、FacebookがMVCを実装することで見逃していたもの-これらのエンティティ間の関係:
- ビューは1つのコントローラーにのみ関連付けることができます
- コントローラーは1つのビューにのみ関連付けることができます。
- モデルはViewとControllerについて何も知らず 、 それらを変更できません
- ビューとコントローラーがモデルにサブスクライブする
- ViewとControllerの1つのペアは、 1つのモデルのみにサブスクライブできます。
- モデルは多くのサブスクライバーを持つことができ、その状態を変更した後にそれらすべてを通知します。
下の画像をご覧ください。 モデルからコントローラーおよびビューに向けられた矢印は、その状態を変更しようとするものではなく、モデルの変更に関する通知です。
元のMVCは、Viewが多くのモデルを変更でき、Modelが多くのViewを変更でき、ControllerはViewと密接な1対1の関係を形成しないというFacebookの実装とはまったく異なります。 さらに、FluxはMVCであり、DispatcherとStoreがモデルの役割を果たし、メソッドを呼び出す代わりにアクションが送信されます。
MVCのプリズムを介して反応する
単純なReactコンポーネントのコードを見てみましょう。
class ExampleButton extends React.Component {
render() { return (
<button onClick={() => console.log("clicked!")}>
Click Me!
</button>
); }
}
Controller'a MVC:
Controller , , View Model
ontroller View
, Controller View ? :
onClick={() => console.log("clicked!")}
Controller, . JavaScript , . React- View, View-Controller.
React, Model. React- Model .
MVC
React-, BaseView, props Model:
// src/Base/BaseView.tsx
import * as React from "react";
import BaseModel from "./BaseModel";
export default class <Model extends BaseModel, Props> extends React.Component<Props & {model: Model}, {}> {
protected model: Model;
constructor(props: any) {
super(props);
this.model = props.model
}
componentWillMount() { this.model.subscribe(this); }
componentWillUnmount() { this.model.unsubscribe(this); }
}
state , . View this.forceUpdate()
, . , , , .
BaseModel, , , :
// src/Base/BaseModel.ts
export default class {
protected views: React.Component[] = [];
subscribe(view: React.Component) {
this.views.push(view);
view.forceUpdate();
}
unsubscribe(view: React.Component) {
this.views = this.views.filter((item: React.Component) => item !== view);
}
protected updateViews() {
this.views.forEach((view: React.Component) => view.forceUpdate())
}
}
TodoMVC , Github.
TodoMVC , . : " ", " ", " ". . :
// src/TodoList/TodoListModel.ts
import BaseModel from "../Base/BaseModel";
import TodoItemModel from "../TodoItem/TodoItemModel";
export default class extends BaseModel {
private allItems: TodoItemModel[] = [];
private mode: string = "all";
constructor(items: string[]) {
super();
items.forEach((text: string) => this.addTodo(text));
}
addTodo(text: string) {
this.allItems.push(new TodoItemModel(this.allItems.length, text, this));
this.updateViews();
}
removeTodo(todo: TodoItemModel) {
this.allItems = this.allItems.filter((item: TodoItemModel) => item !== todo);
this.updateViews();
}
todoUpdated() { this.updateViews(); }
showAll() { this.mode = "all"; this.updateViews(); }
showOnlyActive() { this.mode = "active"; this.updateViews(); }
showOnlyCompleted() { this.mode = "completed"; this.updateViews(); }
get shownItems() {
if (this.mode === "active") { return this.onlyActiveItems; }
if (this.mode === "completed") { return this.onlyCompletedItems; }
return this.allItems;
}
get onlyActiveItems() {
return this.allItems.filter((item: TodoItemModel) => item.isActive());
}
get onlyCompletedItems() {
return this.allItems.filter((item: TodoItemModel) => item.isCompleted());
}
}
. , , . :
// src/TodoItem/TodoItemModel.ts
import BaseModel from "../Base/BaseModel";
import TodoListModel from "../TodoList/TodoListModel";
export default class extends BaseModel {
private completed: boolean = false;
private todoList?: TodoListModel;
id: number;
text: string = "";
constructor(id: number, text: string, todoList?: TodoListModel) {
super();
this.id = id;
this.text = text;
this.todoList = todoList;
}
switchStatus() {
this.completed = !this.completed
this.todoList ? this.todoList.todoUpdated() : this.updateViews();
}
isActive() { return !this.completed; }
isCompleted() { return this.completed; }
remove() { this.todoList && this.todoList.removeTodo(this) }
}
View, Model. View :
// src/TodoList/TodoListInputView.tsx
import * as React from "react";
import BaseView from "../Base/BaseView";
import TodoListModel from "./TodoListModel";
export default class extends BaseView<TodoListModel, {}> {
render() { return (
<input
type="text"
className="new-todo"
placeholder="What needs to be done?"
onKeyDown={(e: any) => {
const enterPressed = e.which === 13;
if (enterPressed) {
this.model.addTodo(e.target.value);
e.target.value = "";
}
}}
/>
); }
}
View, , Controller (props onKeyDown) Model View, Model . props' , .
View TodoListModel, :
// src/TodoList/TodoListView.tsx
import * as React from "react";
import BaseView from "../Base/BaseView";
import TodoListModel from "./TodoListModel";
import TodoItemModel from "../TodoItem/TodoItemModel";
import TodoItemView from "../TodoItem/TodoItemView";
export default class extends BaseView<TodoListModel, {}> {
render() { return (
<ul className="todo-list">
{this.model.shownItems.map((item: TodoItemModel) => <TodoItemView model={item} key={item.id}/>)}
</ul>
); }
}
View , TodoItemModel:
// src/TodoItem/TodoItemView.jsx
import * as React from "react";
import BaseView from "../Base/BaseView";
import TodoItemModel from "./TodoItemModel";
export default class extends BaseView<TodoItemModel, {}> {
render() { return (
<li className={this.model.isCompleted() ? "completed" : ""}>
<div className="view">
<input
type="checkbox"
className="toggle"
checked={this.model.isCompleted()}
onChange={() => this.model.switchStatus()}
/>
<label>{this.model.text}</label>
<button className="destroy" onClick={() => this.model.remove()}/>
</div>
</li>
); }
}
TodoMVC . , 60 . : Model View, . props', . Container-.
Redux?
, Redux , , Redux . frontend- :
- local storage ,
- HTML
- Action'
- undo
Redux , .
Redux , , , . Redux indirection , Presentation Components , Action' State, props. indirection' . , .
indirection' TodoMVC, Redux. State callback' onSave, ?
,hadleSave
TodoItem
props onSave
TodoTextInput
onSave
Enter
, props newTodo
, onBlur
hadleSave
props deleteTodo
, , props editTodo
- props'
deleteTodo
editTodo
TodoItem
MainSection
MainSection
props' deleteTodo
editTodo
TodoItem
- props'
MainSection
App
bindActionCreator
, action' src/actions/index.js
, src/reducers/todos.js
, callback', props', 2 . , .
MVC, , . indirection' , .
Flux Redux MVC, , MVC. Redux , callback' props' , . frontend-, Flux Redux, . . Facebook , "" . frontend- Facebook, . , , MVC ?
UPD
view.setState({})
view.forceUpdate()
. , kahi4.