MVP(Model-View-Presenter)是一种软件架构模式。
MVP模式组成
Model
- 职责:负责数据管理和业务逻辑
- 包含:数据存储、数据获取、业务规则、数据验证等
- 特点:独立于用户界面,可以被多个View复用
View
- 职责:负责用户界面的显示和用户交互
- 包含:UI组件、用户输入处理、界面渲染等
- 特点:被动的,不直接与Model交互,只与Presenter通信
Presenter
- 职责:作为Model和View之间的中介
- 包含:处理用户交互逻辑、格式化数据、控制View更新等
- 特点:包含表示层逻辑,协调Model和View的交互
MVP模式数据流向


代码
用MVP模式实现CounterApp
model层
model应该有让调用方订阅和通知订阅方的能力。
ts
export interface IModel<ModelData, ModelEvent> {
addEventListener<T extends keyof ModelEvent>(type: T, callback: ModelEvent[T]): DisposeHandler;
subscribeModelData(callback: (data: ModelData) => void): DisposeHandler;
getModelData(): ModelData;
}
CounterModel的类型定义
ts
export type CounterModelEvent = {
"data:update": (data: number) => void;
};
export type CounterModelData = number
export interface ICounterModel extends IModel<CounterModelData, CounterModelEvent> {
count : number
}
定义ICounterModel,面相接口编程。
CounterModel的代码实现
ts
export class CounterModel implements ICounterModel
{
eventemitter = new EventEmitter<CounterModelEvent>();
private _count: number = 0;
getModelData() {
return this.count;
}
protected notifyDataUpdate(): void {
this.eventemitter.emit("data:update", this.getModelData());
}
addEventListener<T extends keyof CounterModelEvent>(
type: T,
callback: CounterModelEvent[T]
): DisposeHandler {
this.eventemitter.on(type, callback as any);
return () => {
this.eventemitter.off(type, callback);
};
}
subscribeModelData(
callback: (data: CounterModelData) => void
): DisposeHandler {
return this.addEventListener("data:update", callback);
}
get count() {
return this._count;
}
set count(value: number) {
this._count = value;
this.notifyDataUpdate();
}
}
view层
view应该有订阅业务事件和渲染视图的能力
ts
export interface IView<ViewEvent extends Record<string, Function>, ViewRenderData extends Object> {
addEventListener<T extends keyof ViewEvent>(type: T, callback: ViewEvent[T]): () => void;
render(data: ViewRenderData): void;
destroy(): void;
}
ICounterView类型定义
ts
export type CounterViewEvent = {
"click:increment": () => void;
"click:decrement": () => void;
};
export type CounterViewRenderData = {
count: number;
};
export interface ICounterView extends IView<CounterViewEvent, CounterViewRenderData> {}
CounterView代码
ts
class CounterView implements ICounterView {
private disposeList: DisposeHandler[] = [];
private container: HTMLElement;
private counterContainer!: HTMLDivElement;
private eventemitter = new EventEmitter<CounterViewEvent>();
constructor(container: HTMLElement) {
this.container = container;
this.init();
}
// 初始化视图绑定视图事件
init() {
this.disposeList.push(this.createDecrementButton());
this.createCounterContainer()
this.disposeList.push(this.createIncrementButton());
}
private createIncrementButton() {
const incrementButton = document.createElement("button");
incrementButton.textContent = "Increment";
const handler = () => {
this.eventemitter.emit("click:increment");
};
incrementButton.addEventListener("click",handler );
return () => {
incrementButton.removeEventListener("click", handler);
};
}
private createCounterContainer() {
const counterContainer = document.createElement("div");
counterContainer.textContent = "0";
this.container.appendChild(counterContainer);
}
private createDecrementButton() {
const decrementButton = document.createElement("button");
decrementButton.textContent = "Decrement";
const handler = () => {
this.eventemitter.emit("click:decrement");
};
decrementButton.addEventListener("click", handler);
return () => {
decrementButton.removeEventListener("click", handler);
};
}
addEventListener<T extends keyof CounterViewEvent>(
type: T,
callback: CounterViewEvent[T]
): () => void {
this.eventemitter.on(type, callback);
return () => {
this.eventemitter.off(type, callback);
};
}
render(data: { count: number }) {
this.counterContainer.textContent = data.count.toString();
}
destroy(): void {
this.disposeList.forEach((dispose) => dispose());
this.container.innerHTML = "";
}
}
Presenter
CounterPresenter负责协调counterModel和counterView。
ts
export class CounterPresenter implements IPresenter {
private counterModel: ICounterModel;
private counterView: ICounterView;
constructor(counterModel: CounterModel, counterView: ICounterView) {
this.counterModel = counterModel;
this.counterView = counterView;
}
init() {
this.bindViewEvent();
this.bindModelEvent();
}
bindViewEvent() {
this.counterView.addEventListener("click:increment", () => {
this.counterModel.count += 1;
});
this.counterView.addEventListener("click:decrement", () => {
this.counterModel.count -= 1;
});
}
bindModelEvent() {
this.counterModel.addEventListener("data:update", (data) => {
this.counterView.render({ count: data });
});
}
destroy(): void {
this.counterModel = null;
this.counterView = null;
}
}
CounterApp负责初始化Model,View和Presenter,管理应用的生命周期,对外提供接口。
ts
export class CounterApp {
private counterModel!: CounterModel;
private container: HTMLElement;
private counterView!: CounterView;
private counterPresenter!: CounterPresenter;
constructor(container: HTMLElement) {
this.container = container;
this.createModels();
this.createViews();
this.createPresenters();
this.initPresenters();
}
private createModels() {
this.counterModel = new CounterModel(sameDetector);
}
private createViews() {
this.counterView = new CounterView(this.container);
}
private createPresenters() {
this.counterPresenter = new CounterPresenter(this.counterModel, this.counterView);
}
private initPresenters() {
this.counterPresenter.init();
}
destroy() {
this.counterPresenter.destroy();
this.counterView.destroy();
this.counterModel = null;
}
}
用react的同学都知道,react有批处理机制,这里对model也做一个简单优化,避免一段逻辑触发多次model数据更新通知。
model接口修改添加两个方法
ts
export interface IModel<ModelData, ModelEvent> {
// ......
startBatchUpdate(): void; // 开始批量更新
finishBatchUpdate(): void; // 结束批量更新
}
数据层批处理的代码是类似的,行为可以让子类重写
BaseModel定义批处理流程
ts
export abstract class BaseModel<ModelData, ModelEvent> implements IModel<ModelData, ModelEvent> {
protected _isStartBatchUpdate: boolean = false; // 是否开始批量更新
private beforeStartBatchUpdateModelData: ModelData | null = null; // 批量更新前的模型数据
startBatchUpdate() {
this._isStartBatchUpdate = true; // 开始批量更新
this.beforeStartBatchUpdateModelData = this.getModelData(); // 记录批量更新前的模型数据
}
abstract getModelData(): ModelData;
abstract isSame(oldData: ModelData, newData: ModelData): boolean;
abstract addEventListener<T extends keyof ModelEvent>(type: T, callback: ModelEvent[T]): () => void;
abstract subscribeModelData(callback: (data: ModelData) => void): () => void;
finishBatchUpdate() {
this._isStartBatchUpdate = false; // 结束批量更新
if (this.beforeStartBatchUpdateModelData) {
// 如果有批量更新前的模型数据,则比较是否有变化
if (this.isSame(this.beforeStartBatchUpdateModelData, this.getModelData())) {
return; // 如果没有变化,则不通知数据更新
}
this.beforeStartBatchUpdateModelData = null; // 清空批量更新前的模型数据
}
this.notifyDataUpdate(); // 通知数据更新
}
protected abstract notifyDataUpdate(): void; // 通知数据更新
}
这个例子view,model,presenter复杂度低,没有涉及多个view,model和presenter的情况,虽然比较简单,能通过这个应用理解MVP模式。
模式特点model层和view层解耦,presenter负责协调view层和model层。
优点
- 依赖接口可以方便进行mock测试
- model层独立于view层,可以被多个view层使用
- 数据流向清晰
缺点
- 简单应用容易复杂化
- 事件驱动容易提高调试复杂度,观察者模式也容易提高调试复杂度
- Presenter层承担太多职责容易导致臃肿
- 团队开发成本比较高