观察者模式

目标者对象和观察者对象有相互依赖的关系,观察者对某个对象的状态进行观察,如果对象的状态发生改变,就会通知所有依赖这个对象的观察者,

目标者对象 Subject,拥有方法:添加 / 删除 / 通知 Observer;

观察者对象 Observer,拥有方法:接收 Subject 状态变更通知并处理;

目标对象 Subject 状态变更时,通知所有 Observer。

Vue中响应式数据变化是观察者模式 每个响应式属性都有dep,dep存放了依赖这个属性的watcher,watcher是观测数据变化的函数,如果数据发生变化,dep就会通知所有的观察者watcher去调用更新方法。因此, 观察者需要被目标对象收集,目的是通知依赖它的所有观察者。那为什么watcher也要存放dep呢?是因为当前正在执行的watcher需要知道此时是哪个dep通知了自己。

在beforeCreate之后,created之前调用observe(data)初始化响应式数据,以下是简化版代码(没有处理数组的劫持)

class Observer {
    // 需要对value的属性描述重新定义
    constructor(value) {
      this.walk(value); // 初始化的时候就对数据进行监控
    }
    walk(data) {
      Object.keys(data).forEach((key) => {
        defineReactive(data, key, data[key]);
      });
    }
  }
  
  function defineReactive(data, key, value) {
    // value 可能是一个对象,要递归劫持,所以数据不能嵌套太深
    observe(value);
    let dep = new Dep();
    Object.defineProperty(data, key, {
      get() {
        // 如果有 watcher,就让 watcher 记住 dep,防止产生重复的 dep, 同时 dep 也收集此 watcher
        if (Dep.target) {
          dep.depend();
        }
        return value;
      },
      set(newVal) {
        // 数据没变动则不处理
        if (value === newVal) return;
        observe(newVal); // 如果新值是个对象,递归拦截
        value = newVal; // 设置新的值
        dep.notify(); // 通知收集的 watcher 去更新
      },
    });
}
function observe(data) {
    // 不是对象则不处理,isObject是用来判断是否为对象的函数
    if (Object.prototype.toString.call(data)!== '[object Object]') return;
    // 通过类来实现数据的观测,方便扩展,生成实例
    return new Observer(data);
}
observe(data)

在created之后,mouted之前调用mountComponent挂载组件,以下是简化版代码(没有处理watch和computed的watcher)

class Dep {
    static target = null
    constructor() {
      this.id = id++;
      this.subs = []; // 存放依赖的watcher
    }
    depend() {
      // 让正在执行的watcher记录dep,同时dep也会记录watcher
      Dep.target.addDep(this);
    }
    addSub(watcher) {
      // 添加观察者对象
      this.subs.push(watcher);
    }
    notify() {
      // 触发观察者对象的更新方法
      this.subs.forEach((watcher) => watcher.update());
    }
}
class Watcher {
    constructor(vm, exprOrFn) {
      this.vm = vm;
      this.deps = [];
      // 用来去重,防止多次取同一数据时存入多个相同dep
      this.depId = new Set();
      // exprOrFn是updateComponent
      this.getter = exprOrFn;
      // 更新页面
      this.get();
    }
    get() {
      Dep.target = watcher; // 取值之前,收集 watcher
      this.getter.call(this.vm); // 调用updateComponent更新页面
      Dep.target = null; // 取值完成后,将 watcher 删除
    }
    // dep.depend执行时调用
    addDep(dep) {
        let id = dep.id;
        let has = this.depId.has(id);
        if (!has) {
            this.depId.add(id);
            // watcher存放dep
            this.deps.push(dep);
            // dep存放watcher
            dep.addSub(this);
        }
    }  
    // 更新页面方法,dep.notify执行时调用
    update() {
        this.get(); // 一修改数据就渲染更新
    }
}
function mountComponent(vm) {
    // 渲染更新页面
    let updateComponent = () => {
      let vnode = vm._render(); // 生成虚拟节点 vnode
      vm._update(vnode); // 将vnode转为真实节点
    };
    // 每个组件都要调用一个渲染 watcher
    new Watcher(vm, updateComponent);
}
mountComponent(vm)

 

发布订阅模式

基于一个事件中心,接收通知的对象是订阅者,需要 先订阅某个事件,触发事件的对象是发布者,发布者通过触发事件,通知各个订阅者。 js中事件绑定,就是发布订阅模式

发布订阅模式相比观察者模式多了个事件中心,订阅者和发布者不是直接关联的。

vue中的事件总线就是使用的发布订阅模式

 

// 事件总线
class Bus {
  constructor() {
    // 用来记录事件和监听该事件的数组
    this.listeners = {};
  }
  // 添加指定事件的监听者
  $on(eventName, handler) {
    this.listeners[eventName].add(handler);
  }
  // 取消监听事件
  $off(eventName, handler) {
    this.listeners[eventName].delete(handler);
  }
  // 触发事件
  $emit(eventName, ...args) {
    this.listeners[eventName].forEach((fn) => fn(...args));
  }
}

复制代码

事件总线的具体使用可以查看这篇vue之事件总线

观察者模式和发布订阅模式的区别

目标和观察者之间是互相依赖的。

发布订阅模式是由统一的调度中心调用,发布者和订阅者不知道对方的存在。

Logo

前往低代码交流专区

更多推荐