JavaScript 设计模式(一)观察者模式与发布订阅模式

67 阅读4分钟

观察者模式与发布订阅模式,本质上是一样的,但是在设计思想上还是有一定差别的。

观察者模式

基本介绍

首先来看一下观察者定义,了解一下什么是观察者模式。

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。

image.png

观察者模式中定义了一种一对多的关系,这里的我们就可以称之为上图中的目标对象Subject称之为上图中的观察者对象Observer,它可以接收目标对象Subject状态的改变并进行处理。 举个例子:

学校铃声是目标对象,学校里的老师和学生是观察对象,当上课铃声响起时,老师和同学会接收到上课的信息,然后进行上课准备;当下课铃声响起时候,老师和同学会接收下课的信息,进行课下活动...

代码案例

下面可以通过具体的代码案例来深入了解目标对象和观察者对象是如何进行联系的:

// 定义一个目标对象
class Subject {
    constructor() {
        this.Observers = [];
    }
    add(observer) {
        // 添加
        this.Observers.push(observer);
    }
    remove(observer) {
        // 移除
        this.Observers.filter((item) => item === observer)
    }
    notify() {
        // 通知所有观察者
        this.Observers.forEach((item) => {
            item.update();
        })
    }
}
// 定义观察者对象
class Observer {
    constructor(name) {
        this.name = name;
    }
    update() {
        console.log(`name: ${this.name}`)
    }
}

let sub = new Subject();
let obs1 = new Observer('observer1');
let obs2 = new Observer('observer2');
sub.add(obs1);
sub.add(obs2);
sub.remove(obs2);
sub.notify();

项目中实际应用

观察者模式在我们的项目应用中也是很常见的一种模式,比如:

dom元素监听事件

给dom元素绑定click监听事件,本质上就是观察者模式。

var btn = document.getElementById('btn')
btn.addEventListener('click', function(ev){
  console.log(1)
})
btn.addEventListener('click', function(ev){
  console.log(2)
})

这里btn元素可以看作我们的目标对象(被观察的对象),当它被点击时,也就是它的状态发生了改变,那么我们通过addEventListener函数添加的两个匿名函数(观察者)就会观测到改变。

IntersectionObserver 交叉观察器

除了click监听事件,我们在项目中还会经常遇到这种需求:监测某个元素是否滚动到视口范围内,传统的实现方法是,监听到scroll事件后,调用目标元素的getBoundingClientRect()方法,得到它对于视口(浏览器窗口)左上角的坐标,再判断是否在视口之内。存在缺点是,scroll事件密集发生,计算量很大,容易造成性能问题。

浏览器为我们提供了一个新的IntersectionObserver API,可以自动观察到元素是否可见。

API基本使用

// 创建 IntersectionObserver 对象
var io = new IntersectionObserver(callback, option);
// 开始观察一个目标元素
io.observe(document.getElementById('example'));
// 停止观察
io.unobserve();
// 关闭观察器
io.disconnect();

IntersectionObserver是浏览器提原生提供的构造函数,接收两个参数:

callback,目标元素的可见性发生变化时,就会调用观察器的回调函数callback。

var io = new IntersectionObserver((entries) => {
    console.log(entries);
    entries.forEach(i => {
       })
});

callback函数的参数(entries)是一个数组,是被观察的对象(目标元素),如果同时有两个被观察的对象,那么entries就会有两个成员,每个成员都是IntersectionObserverEntry对象。

IntersectionObserverEntry 属性含义
time可见性发生变化的时间,是时间戳,毫秒
target被观察的元素,目标元素,是一个DOM节点对象
rootBounds目标元素的根元素的区域信息,getBoundingClientRect()的返回值
isIntersecting返回一个布尔值,目标元素可见返回true,反之返回false
intersectionRect目标元素与视口(或跟元素)的交叉区域信息
intersectionRatio目标元素的可见比例,即 intersectionRect 与 boundingClientRect 的比例,完全可见1,完全不可见0
boundingClientRect目标元素的矩形区域信息,边界的计算方式与  Element.getBoundingClientRect() 相同.

image.png

上图中,灰色的水平方框代表视口,深红色的区域代表四个被观察的目标元素。

option是配置参数,可选,可以设置以下几个值:

  • thresholds,一个包含阈值的列表,决定了什么时候触发回调函数,是一个数组,每个成员都是一个门槛值,默认为[0],即交叉比例(intersectionRatio)达到0时触发函数。
new IntersectionObserver(
  entries => {/* ... */}, 
  {
    threshold: [0, 0.25, 0.5, 0.75, 1]
  }
);

[0, 0.25, 0.5, 0.75, 1]就表示目标元素 0%,25%,75%,100% 可见时会触发回调函数。

  • root 属性,rootMargin 属性

    root属性指定目标元素的具体祖先元素即目标元素所在的容器节点(根元素)。如果未传入值或值为 null,则默认使用顶级文档的视窗。

    rootMargin属性定义根元素的margin,用来扩展或者缩小rootBounds这个矩形的大小,从而影响intersectionRect交叉区域的大小。定义和css方法一致,比如10px 20px 30px 40px,表示 top、right、bottom 和 left 四个方向的值。

var opts = { 
  root: document.querySelector('.container'),
  rootMargin: "500px 0px" 
};

var observer = new IntersectionObserver(
  callback,
  opts
);