嵌套元素下Observer模式的实现困境与优化探讨
问题描述
原问题
用原生JS实现无Controller的Observer模式,要求Model数据变化时自动重渲染关联视图。当前核心问题:每次重渲染都会重复注册观察者——必须把view引用传入Model的registerObserver,通过includes判断是否已注册,但父视图重渲染时会丢失嵌套视图的引用,导致重复注册问题。
更新进展
在构建「父视图负责渲染嵌套子视图+管理观察者注册/注销」的层级结构时遇到阻碍:无法获取每个视图直接子元素的引用,重渲染时没法判断它们是否已在Model中注册。目前考虑放弃当前模式,转而使用自定义事件,在文档片段创建时添加监听器,确保重渲染时能正常绑定/移除监听器。
相关代码
父视图代码
class View { nestedViews = {}; self = this; constructor(parentEl, model){ this.model = model; this.parentEl = parentEl; } update() { this.render(); } render() { // 子类实现的渲染逻辑 this.onRender(); // 绑定Model this.bindModel(); //... 将元素插入DOM的逻辑 } }
嵌套视图代码
class NestedView extends View { bindModel() { Object.entries(this.nestedViews).forEach(([key, value]) => this.model.registerObserver(value) ); } onRender() { this.nestedViews['nestedView1'] = new NestedView1( parent, this.model ); this.nestedViews['nestedView1'].render(); this.nestedViews['nestedView2'] = new NestedView2( parent, this.model ); this.nestedViews['nestedView2'].render(); }}
Model代码
class Model { observers = []; registerObserver(observer) { if (!this.observers.includes(observer)) this.observers.push(observer); } unregisterObserver(observer){ this.observers = this.observers.filter((obs) => obs !== observer); } updateObservers() { this.observers.forEach((observer) => observer.update()); } set() { // 设置属性逻辑 this.updateObservers(); } }
解决方案
方案1:优化Observer模式,让视图自管理注册/注销
核心思路:每个视图实例在初始化时注册观察者,重渲染前先注销旧的嵌套视图和自身的观察者,从根源避免重复注册。
修改View类
class View { nestedViews = {}; constructor(parentEl, model){ this.model = model; this.parentEl = parentEl; // 初始化时注册当前视图到Model this.model.registerObserver(this); } // 新增:清理嵌套视图和观察者的方法 cleanup() { // 递归清理所有子视图的观察者 Object.values(this.nestedViews).forEach(view => { this.model.unregisterObserver(view); view.cleanup(); }); // 清空嵌套视图引用 this.nestedViews = {}; } update() { // 重渲染前先清理旧视图和观察者 this.cleanup(); this.render(); } render() { this.onRender(); // 移除原bindModel调用,子视图在constructor中已自动注册 } }
调整NestedView逻辑
class NestedView extends View { onRender() { // 注意:这里的parent要指向当前视图下的容器元素,比如this.parentEl内的节点 const parent = this.parentEl.querySelector('.nested-container'); this.nestedViews['nestedView1'] = new NestedView1(parent, this.model); this.nestedViews['nestedView1'].render(); this.nestedViews['nestedView2'] = new NestedView2(parent, this.model); this.nestedViews['nestedView2'].render(); } }
Model代码保留核心逻辑
原Model的registerObserver和unregisterObserver逻辑无需修改,includes判断能确保同一实例不会被重复添加。
方案2:切换为自定义事件模式
如果觉得Observer模式的层级管理太麻烦,用自定义事件更灵活,重渲染时只需解绑再绑定即可:
修改Model类
class Model { constructor() { this.eventEmitter = new EventTarget(); } set(attr, value) { this[attr] = value; // 触发数据更新事件,携带变化的属性信息 this.eventEmitter.dispatchEvent(new CustomEvent('dataUpdated', { detail: { attr, value } })); } }
修改View类
class View { constructor(parentEl, model){ this.model = model; this.parentEl = parentEl; this.bindEvents(); } bindEvents() { // 先解绑旧监听,避免重复绑定 this.model.eventEmitter.removeEventListener('dataUpdated', this.handleUpdate); // 绑定新监听 this.handleUpdate = () => this.update(); this.model.eventEmitter.addEventListener('dataUpdated', this.handleUpdate); } update() { this.render(); // 重渲染后重新绑定事件(可选,视场景而定) this.bindEvents(); } //... 其他渲染方法 }
关键注意点
- 谁创建谁销毁:视图实例创建时注册观察者/绑定事件,销毁/重渲染前注销/解绑,避免内存泄漏和重复触发
- 递归清理:父视图清理时必须递归处理子视图,确保所有嵌套层级的观察者都被正确注销
- 自定义事件优势:无需维护观察者列表,事件系统自动管理绑定关系,更适合复杂嵌套结构
内容的提问来源于stack exchange,提问作者NickW
相关产品推荐
相关产品推荐

