You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

嵌套元素下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();
  }

  //... 其他渲染方法
}

关键注意点

  1. 谁创建谁销毁:视图实例创建时注册观察者/绑定事件,销毁/重渲染前注销/解绑,避免内存泄漏和重复触发
  2. 递归清理:父视图清理时必须递归处理子视图,确保所有嵌套层级的观察者都被正确注销
  3. 自定义事件优势:无需维护观察者列表,事件系统自动管理绑定关系,更适合复杂嵌套结构

内容的提问来源于stack exchange,提问作者NickW

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 14:27:19