保姆式教程,带你走进原生观察者Observer 大家族,通俗易懂

1,053 阅读9分钟

开源项目推荐

ant-simple-pro一款用vite构建的中台解决方案,支持vue3reactangulartypescript等。

jol-player一款简洁,美观,功能强大的react播放器

前言

自从vue出来之后,发布模式订阅模式观察者模式好像被大多数人所熟悉,如下图👇

data.png

但是上面毕竟是一种设计模式,而且对于设计模式,面向对象编程不是很熟悉的话,有点不太好理解的,那么原生的JavaScript有提供这样的对象吗😶,肯定有啦javascript不在是以前那个弱不禁风的语言了。

MutationObserver(变更观察者)

MutationObserver接口提供了监视对DOM树所做更改的能力。它被设计为旧的Mutation Events功能的替代品,该功能是DOM3 Events规范的一部分。

来源于mozilla

1. 概念

可以监听一个DOM树上节点的变化,以及一些属性的变更,它会在触发指定 DOM 事件时,调用指定的回调函数。MutationObserver 对 DOM 的观察不会立即启动;而必须先调用 observe() 方法来确定,要监听哪一部分的 DOM 以及要响应哪些更改。

2. 语法

const observer = new MutationObserver(callback);

参数

  • callback

    一个回调函数,每当被指定的节点或子树以及配置项有Dom变动时会被调用。回调函数拥有两个参数:一个是描述所有被触发改动的 MutationRecord 对象数组,另一个是调用该函数的MutationObserver 对象。

方法
  • disconnect()

    阻止 MutationObserver 实例继续接收的通知,直到再次调用其observe()方法,该观察者对象包含的回调函数都不会再被调用。

  • observe()

    配置MutationObserver在DOM更改匹配给定选项时,通过其回调函数开始接收通知。

  • takeRecords()

    从MutationObserver的通知队列中删除所有待处理的通知,并将它们返回到MutationRecord对象的新Array中。

3. 使用

本文章的demo案例采用typescriptreactantd来进行编写演示。

基础用法
import React, { memo, useEffect, useState } from 'react';
import { Button, Space } from 'antd';
const MutationObservers = memo(function MutationObservers() {
 const [list, setList] = useState<string[]>([]);

 useEffect(() => {
   // 选择需要观察变动的节点
   const targetNode = document.getElementById('mutation');

   // 观察器的配置(需要观察什么变动)
   const config = { attributes: true, childList: true, subtree: true };

   // 当观察到变动时执行的回调函数
   const callback: MutationCallback = function (mutationsList, observer) {
     mutationsList.forEach((mutation) => {
       switch (mutation.type) {
         case 'childList':
           if (mutation.addedNodes.length) {
             console.log('有一个节点被添加');
           }
           if (mutation.removedNodes.length) {
             console.log('有一个节点被移除');
           }
           break;
         case 'attributes':
           console.log('这个 ' + mutation.attributeName + ' 属性有被改变过');
           break;
         case 'characterData':
           console.log('characterData节点变化');
           break;
       }
     });
   };

   // 创建一个观察器实例并传入回调函数
   const observer = new MutationObserver(callback);

   // 以上述配置开始观察目标节点
   observer.observe(targetNode!, config);

   return () => {
     //  之后,可停止观察
     observer.disconnect();
   };
 }, []);
 return (
   <>
     <Button type="primary" onClick={() => setList((pre) => [...pre, '节点'])}>
       添加元素
     </Button>
     <div id="mutation">
       {list.map((item, index) => (
         <div key={index}>
           <Space>
             <span>
               {item}
               {index + 1}
             </span>
             <Button
               danger
               size="small"
               onClick={() => setList(list.filter((item, k) => k !== index))}
             >
               删除
             </Button>
           </Space>
         </div>
       ))}
     </div>
   </>
 );
});

export default MutationObservers;

20211123_230504.gif

4. 应用场景

  • 监听JS脚本创建DOM渲染是否完成

  • 监听Dom节点的变化,属性等

  • 异步动态创建节点,执行一些操作

IntersectionObserver(交集观察者)

1. 概念

搜狗截图20211124161453.png IntersectionObserver会注册一个回调函数,每当被监视的元素进入或者退出另外一个元素时(或者 viewport ),或者两个元素的相交部分大小发生变化时,该回调方法会被触发执行。这样,我们网站的主线程不需要再为了监听元素相交而辛苦劳作,浏览器会自行优化元素相交管理。

注意

IntersectionObserver无法提供重叠的像素个数或者具体哪个像素重叠,他的更常见的使用方式是——当两个元素相交比例在 N% 左右时,触发回调,以执行某些逻辑。

2. 语法

const observer = new IntersectionObserver(callback[, options]);
参数
  • callback 当元素可见比例超过指定阈值后,会调用一个回调函数,此回调函数接受两个参数:
  • options 可选 一个可以用来配置observer实例的对象。如果options未指定,observer实例默认使用文档视口作为root,并且没有margin,阈值为0%(意味着即使一像素的改变都会触发回调函数)。你可以指定以下配置:
    • root 监听元素的祖先元素Element对象,其边界盒将被视作视口。目标在根的可见区域的的任何不可见部分都会被视为不可见。

    • rootMargin 一个在计算交叉值时添加至根的边界盒(bounding_box (en-US))中的一组偏移量,类型为字符串(string) ,可以有效的缩小或扩大根的判定范围从而满足计算需要。语法大致和CSS 中的margin 属性等同; 可以参考 The root element and root margin in Intersection Observer API来深入了解margin的工作原理及其语法。默认值是"0px 0px 0px 0px"。

    • threshold

      规定了一个监听目标与边界盒交叉区域的比例值,可以是一个具体的数值或是一组0.0到1.0之间的数组。若指定值为0.0,则意味着监听元素即使与根有1像素交叉,此元素也会被视为可见. 若指定值为1.0,则意味着整个元素都在可见范围内时才算可见。可以参考Thresholds in Intersection Observer API 来深入了解阈值是如何使用的。阈值的默认值为0.0。

方法
  • disconnect()

    终止对所有目标元素可见性变化的观察。

  • observe()

    IntersectionObserver对象监听的目标集合添加一个元素。一个监听者有一组阈值和一个根, 但是可以监视多个目标元素,以查看这些目标元素可见区域的变化

  • takeRecords()

    返回一个 IntersectionObserverEntry 对象数组, 每个对象的目标元素都包含每次相交的信息, 可以显式通过调用此方法或隐式地通过观察者的回调自动调用.

  • unobserve()

    停止对一个元素的观察

3. 使用

基础用法
import React, { memo, useEffect } from 'react';

const IntersectionComponent = memo(function IntersectionComponent() {
useEffect(() => {
 const callback: IntersectionObserverCallback = (entries) => {
   entries.forEach((item) => {
     if (item.isIntersecting) {
       console.log(`可见`);
     } else {
       console.log(`不可见`);
     }
   });
   console.log(`entries`, entries);
 };
 const intersection = new IntersectionObserver(callback);

 document.querySelector('#scrollerFooter') &&
   intersection.observe(document.querySelector('#scrollerFooter')!);
 return () => {
   intersection.disconnect();
 };
}, []);
return (
 <div>
   <p>ant-simple-pro</p>
    {/* 省略了<p>ant-simple-pro</p>*/}
   <div id="scrollerFooter">我要出来啦</div>
 </div>
);
});
export default IntersectionComponent;

22.gif

写个导航栏定位功能
import React, { memo, useEffect, useState } from 'react';
const Anchor = memo(function Anchor() {
const [dataKey, setDataKey] = useState<string | null>(null);
const navList = [
 { title: '家电区', bg: '#ff000026', id: 'homeAppliances' },
 { title: '服装区', bg: '#9c27b03d', id: 'clothing' },
 { title: '美食区', bg: '#3f51b547', id: 'delicacy' },
 { title: '生活区', bg: '#03a9f442', id: 'life' },
 { title: '婴儿区', bg: '#ff572238', id: 'baby2' },
];
const getStyle = (bg: string): React.CSSProperties => ({
 background: bg,
 height: '700px',
 width: '70%',
 margin: '20px 0',
});
const positioningNavigation = (id: string) => {
 const element = document.querySelector(`#${id}`);
 element && element.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
 const callback: IntersectionObserverCallback = (entries) => {
   entries.forEach((item) => {
     if (item.isIntersecting) {
       setDataKey(item.target.dataset.key);
     }
   });
 };
 const intersection = new IntersectionObserver(callback, { threshold: 0.8 });

 navList.forEach((item) => {
   const element = document.querySelector(`#${item.id}`);
   element && intersection.observe(element);
 });
 return () => {
   intersection.disconnect();
 };
}, []);
return (
 <div>
   {navList.map((item, index) => (
     <div style={getStyle(item.bg)} id={item.id} key={index} data-key={item.id}>
       <h2 style={{ textAlign: 'center' }}>{item.title}</h2>
     </div>
   ))}
   <ul
     style={{
       position: 'fixed',
       top: '20%',
       right: '50px',
       border: '1px solid #ccc',
       textAlign: 'center',
     }}
   >
     {navList.map((item) => (
       <li
         key={item.id}
         style={{
           padding: '10px',
           cursor: 'pointer',
           background: dataKey === item.id ? 'red' : 'none',
         }}
         onClick={() => positioningNavigation(item.id)}
       >
         {item.title}
       </li>
     ))}
   </ul>
 </div>
);
});

export default Anchor;

20211124_173051.gif

4. 应用场景

  • 图片懒加载
  • 内容无限滚动
  • 定位导航栏(如上案例)
  • 在用户看见某个区域时执行任务或播放动画,视频等操作

ResizeObserver(视图观察者)

1. 概念

ResizeObserver提供了一种高性能的机制,通过该机制,代码可以监视元素的大小更改,并且每次大小更改时都会向观察者传递通知,接口可以监听到 Element 的内容区域或 SVGElement的边界框改变。内容区域则需要减去内边距padding。(有关内容区域、内边距资料见盒子模型 )。

2. 语法

const ResizeObserver = new ResizeObserver(callback)
参数
  • callback 当尺寸发生变化时触发回调,使用ResizeObserverEntry对象数组调用该方法。
方法

3. 使用

import React, { memo, useEffect } from 'react';

const ResizeObserverComponent = memo(function ResizeObserverComponent() {
useEffect(() => {
 const callback: ResizeObserverCallback = (entries) => {
   entries.forEach((item) => {
     if (item.contentRect) {
       const { width } = item.contentRect;
       item.target.style.fontSize = Math.ceil(width / 14) + 'px';
     }
   });
 };
 const resizeObserver = new ResizeObserver(callback);
 document.querySelector('#resize')! &&
   resizeObserver.observe(document.querySelector('#resize')!);
 return () => {
   resizeObserver.disconnect();
 };
}, []);
return (
 <>
   <div id="resize" style={{ textAlign: 'center', lineHeight: '150px' }}>
     ant-simple-pro
   </div>
 </>
);
});
export default ResizeObserverComponent;

20211125_152801.gif 如上案例,会当divwidth改变的时候,会重新修改fontSize的样式,very good 😏

4. 应用场景

  • 配合媒介查询 / window.matchMedia实现更加强大的响应式布局

  • 可以针对单个元素的改变,而重新整体的布局改变

  • 简单的布局,不在依赖响应式设计理念,ResizeObserver就可以做到

PerformanceObserver(性能观察者)

1. 概念

PerformanceObserver 用于监测性能度量事件,在浏览器的性能时间轴记录下一个新的 performance entries  的时候将会被通知,给定的观察者 callback 生成一个新的 PerformanceObserver 对象.当通过 observe() 方法注册的 条目类型 的 性能条目事件 被记录下来时,调用该观察者回调 。

2. 语法

const observer = new PerformanceObserver(callback);
参数
语法
  • disconnect()

    用于阻止性能观察者接收任何 性能条目 事件。

  • observe()

    用于观察传入的参数中指定的性能条目类型的集合。当记录一个指定类型的性能条目时,性能监测对象的回调函数将会被调用。

  • takeRecords()

    返回当前存储在性能观察器中的 性能条目  列表,将其清空

3. 使用

import React, { memo, useEffect } from 'react';
import { Button } from 'antd';
const PerformanceObserverComponent = memo(function PerformanceObserverComponent() {
 useEffect(() => {
   const observer = new PerformanceObserver((list) => {
     list.getEntries().forEach((entry) => {
       console.log(
         '名称: ' +
           entry.name +
           ', 类型: ' +
           entry.entryType +
           ', 开始时间: ' +
           entry.startTime +
           ', 时间: ' +
           entry.duration +
           '\n',
       );
     });
   });
   observer.observe({ entryTypes: ['resource', 'mark', 'measure'] });
   performance.mark('registered-observer');
   return () => {
     observer.disconnect();
   };
 }, []);
 const trigger = () => {
   performance.measure('button clicked');
 };
 return (
   <>
     <Button danger size="small" onClick={trigger}>
       点击我看性能
     </Button>
   </>
 );
});
export default PerformanceObserverComponent;

20211125_172417.gif 我们可以通过页面加载,和点击按钮,可以检测到页面的响应速度性能,very good 😏

4. 应用场景

  • 可以用来检测页面性能,静态资源等
  • 可以用来作为开发性能SDK插件
  • 可以用来做性能可视化

结论

MutationObserverIntersectionObserverResizeObserverPerformanceObserver这四个对象,大家可以尝试用到自已的项目中去,前提是不兼容低端游览器MutationObserverResizeObserver这两个对象,建议小伙伴们必须掌握和知道,因为用的较多,vue中就用到了MutationObserver