likes
comments
collection
share

浏览器原生提供的观察者,是真的好用!

作者站长头像
站长
· 阅读数 3

JavaScript中的观察者模式(Observer pattern)是一种设计模式,用于定义一种一对多的依赖关系。当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。这种模式在实现松耦合和可维护性方面非常有用。

监控滑动是否style,调整样式,实现效果:

浏览器原生提供的观察者,是真的好用!

理解观察者模式

观察者模式又名发布-订阅(Publish/Subscribe)模式,它允许观察者在接收到通知后执行各种操作,具体取决于其实现的 update 方法和业务需求。

简单的JavaScript观察者模式示例

按照自定义观察者模式,我们可以分为两个主要角色:

目标角色(Subject)

// 主题
class Subject {
  constructor() {
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify() {
    this.observers.forEach(observer => observer.update());
  }
}

观察者角色(Observer)

// 观察者
class Observer {
  constructor(name) {
    this.name = name;
  }

  update() {
    console.log(`${this.name} 收到通知,进行更新操作。`);
  }
}

使用示例:

// 使用示例
const subject = new Subject();

const observer1 = new Observer('观察者1');
const observer2 = new Observer('观察者2');

subject.addObserver(observer1);
subject.addObserver(observer2);

// 主题状态发生变化时,通知所有观察者(轮询调用了Observer中的update(),)
subject.notify();

// 输出:
// 观察者1 收到通知,进行更新操作。
// 观察者2 收到通知,进行更新操作。

当调用Subjectnotify方法时,所有注册的观察者会收到通知并执行其更新操作。

这是一个简单的自定义观察者模式的例子,但在实际开发中,我们通常会使用浏览器原生提供的观察者模式来处理特定的场景。

浏览器原生提供的观察者模式

浏览器原生提供了多种观察者模式的实现,其中之一是 MutationObserver。它具有以下优点:

  1. 实时捕捉变化: MutationObserver 允许你在 DOM 发生变化的瞬间得到通知,而不需要定期轮询 DOM 元素。这样可以确保你在最早的时候获取到变化。
  2. 性能优化: 相较于其他检测 DOM 变化的方法,如定时器轮询或事件监听,MutationObserver 更为高效,因为仅在真正有变化时才触发回调。
  3. 支持灵活的配置: 可以配置 MutationObserver 以观察特定类型的变化,比如属性变化、子节点变化等,非常灵活适应不同监测需求。

除了 MutationObserver,还有其他一些观察者模式的实现,如 IntersectionObserver 用于观察目标元素与其祖先元素或视窗的交叉情况,以及 ResizeObserver 用于观察元素的大小变化。

在现代Web开发中,这些观察者模式通常被用于实现更高效和性能友好的交互和动态效果。

应用示例:Vant中的PickerNumber

以Vant中的 PickerNumber 组件为例,该组件在滑动 column 时,实际上是通过 ul 中的 transform 进行动画处理。

浏览器原生提供的观察者,是真的好用!

我们的目标是在触摸滑动的时候,能让我们滑动到中间的文字,进行选中变粗等样式修改,比如下面这样:

浏览器原生提供的观察者,是真的好用!

我们接下来就使用浏览器原生提供的观察者模式(Observer pattern)来完成这些处理,具体以 MutationObserver 为例:

获取DOM:

getPickerDom() {
  // 变白色
  const pickerColumnsDom = document.querySelector(".picker-number .van-picker .van-picker__columns");
  const pickerColumnDom = pickerColumnsDom.querySelectorAll(".van-picker-column");
  const ulPickerColumnDom = Array.from(pickerColumnDom).map(res => {
     return res.querySelector(".van-picker-column__wrapper");
  });
  return ulPickerColumnDom;
},

处理滑动事件:

handleTouchMove(dom) {
  // 处理滑动事件的函数

  // 获取当前滑动的位置
  const translateY = this.getTranslateY(dom);
  // 获取所有列表项
  const items = dom.querySelectorAll(".van-picker-column__item");
  // 获取单个列表项的高度
  const itemHeight = items.length > 0 ? items[0].offsetHeight : 32;
  // 当前初始化的高度
  const itemOffset = dom.offsetHeight / 2;
  // 计算当前滑动位置对应的列表项索引
  // this.initCount picker-nunber默认首次出现的位置
  const currentIndex = (Math.round((itemOffset - translateY) / (itemHeight))) - this.initCount + 3;
  // 移除所有列表项的选中状态
  items.forEach((item, index) => {
    item.classList.remove("van-picker-column__item--selected");
    // 添加选中状态给当前索引的列表项
    if (index === currentIndex) {
      item.classList.add("van-picker-column__item--selected");
    }
  });
},
getTranslateY(element) {
  // 获取元素的纵向位移值
  const transform = window.getComputedStyle(element).getPropertyValue("transform");
  const translateYMatch = transform.split(", ");
  const translateY = translateYMatch[translateYMatch.length - 1].slice(0, -1);
  return translateYMatch ? translateY : 0;
}

创建观察者实例:

监听ul中style中transfrom的变化,配置只观察style属性的变化

mutationObserver(targetElement) {
    // 创建一个MutationObserver实例
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if (mutation.attributeName === 'style') {
                // 处理咱们事件 比如我自己的处理事件
                 this.handleTouchMove(targetElement);
            }
        });
    });

    // 配置观察选项
    const observerConfig = {
        attributes: true, // 观察属性的变化
        attributeFilter: ['style'] // 只观察style属性的变化
    };

    // 开始观察目标元素
    observer.observe(targetElement, observerConfig);
    // 停止观察
    // observer.disconnect();
},

绑定观察者:

bindObserver() {
    const  arrayDom = this.getPickerDom();
    for(let dom of arrayDom){
        this.mutationObserver(dom)
    }
},

启动:

mounted(){
   this.bindObserver();
},

我们使用 MutationObserver 充当观察者,通过监听 style 属性的变化来实时捕捉滑动事件,然后调用 handleTouchMove 方法进行相应的处理。这样的模式使得观察逻辑解耦,代码更加灵活和