MVP模式中View和Presenter怎么解耦才不会互相引用?

UE丶光远 阅读 94

我在用原生JS写一个简单的MVP架构,但发现View里要调用Presenter的方法,Presenter又要操作View的DOM,结果两边互相持有对方引用,感觉耦合太紧了。

比如我的View初始化时会this.presenter = new Presenter(this),而Presenter里又存了view实例去更新UI。这样真的符合MVP吗?有没有更干净的解耦方式?

我试过用事件监听,但逻辑一复杂就乱了。现在代码大概是这样:

class View {
  constructor() {
    this.presenter = new Presenter(this);
    this.button = document.getElementById('btn');
    this.button.addEventListener('click', () => {
      this.presenter.handleButtonClick();
    });
  }

  updateText(text) {
    this.button.textContent = text;
  }
}

class Presenter {
  constructor(view) {
    this.view = view;
  }

  handleButtonClick() {
    this.view.updateText('Clicked!');
  }
}
我来解答 赞 10 收藏
二维码
手机扫码查看
2 条解答
静静 Dev
解决MVP架构中View和Presenter之间的耦合问题,我们可以采用观察者模式或者通过接口来实现更松耦合的设计。具体来说,这里我推荐使用观察者模式,因为它可以很好地避免直接引用,同时保持功能的完整。

首先,我们需要定义一个简单的事件总线(Event Bus),它允许对象订阅和发布事件而不直接相互引用。这样View和Presenter就可以通过这个事件总线来进行通信,而不需要直接持有对方的引用。

接下来,我们来重构你的代码,引入一个事件总线:

// 定义一个简单的事件总线
class EventBus {
constructor() {
this.events = {};
}

// 订阅事件
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}

// 发布事件
emit(eventName, data) {
const event = this.events[eventName];
if (event) {
event.forEach(callback => callback(data));
}
}
}

// 创建一个全局的事件总线实例
const eventBus = new EventBus();

class View {
constructor() {
this.button = document.getElementById('btn');
// 订阅来自Presenter的事件,更新UI
eventBus.on('updateText', (text) => this.updateText(text));

// 当按钮被点击时,发布事件给Presenter
this.button.addEventListener('click', () => {
eventBus.emit('buttonClicked');
});
}

updateText(text) {
this.button.textContent = text;
}
}

class Presenter {
constructor() {
// 订阅来自View的事件,处理业务逻辑
eventBus.on('buttonClicked', () => this.handleButtonClick());
}

handleButtonClick() {
// 处理点击逻辑后,发布事件给View更新UI
eventBus.emit('updateText', 'Clicked!');
}
}

// 初始化View和Presenter,不再需要直接传递引用
new View();
new Presenter();


在这个重构后的版本中,ViewPresenter 之间不再直接持有对方的引用。取而代之的是,它们通过 eventBus 来发布和订阅事件。这样做的好处是,双方完全解耦,任何一方的变化都不会直接影响到另一方,只要它们继续按照约定的事件名称和数据格式进行通信即可。

这种方法不仅减少了类与类之间的直接依赖,还能使代码更加模块化和易于维护。当然,对于小型项目或者简单的交互,这种过度设计可能显得有些多余,但对于大型应用来说,这种模式的优势会非常明显。
点赞
2026-03-24 02:01
Des.耘郗
你这种互相引用的方式确实有点问题,虽然能跑但耦合太重。我给你个更干净的方案,用事件总线来解耦,效率更高而且符合MVP原则。

核心思路是把双向引用变成单向数据流:View只触发事件,Presenter监听事件并处理业务逻辑,然后通过事件通知View更新。这样两边都不会直接引用对方。

// 简单实现事件总线
const EventBus = {
events: {},
emit(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(cb => cb(data));
},
on(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
}
};

class View {
constructor() {
this.button = document.getElementById('btn');
this.button.addEventListener('click', () => {
EventBus.emit('buttonClicked');
});

EventBus.on('updateText', text => {
this.button.textContent = text;
});
}
}

class Presenter {
constructor() {
EventBus.on('buttonClicked', () => {
this.handleButtonClick();
});
}

handleButtonClick() {
EventBus.emit('updateText', 'Clicked!');
}
}

// 初始化
new View();
new Presenter();


几点关键改进:
1. View和Presenter完全不知道对方的存在,只和事件总线交互
2. View负责渲染和事件触发,Presenter负责业务逻辑
3. 数据流动方向清晰:View -> Event -> Presenter -> Event -> View

比起你的原始方案,这种实现更符合MVP的核心思想,而且扩展性更好。如果后面要加新功能,只需要增加新的事件类型就行,不用改现有代码。
点赞
2026-03-09 01:02