松鼠乐园 松鼠乐园
  • 注册
  • 登录
  • 首页
  • 快捷入口
    • Vue
    • Tensorflow
    • Springboot
    • 语言类
      • CSS
      • ES5
      • ES6
      • Go
      • Java
      • Javascript
    • 工具类
      • Git
      • 工具推荐
    • 服务器&运维
      • Centos
      • Docker
      • Linux
      • Mac
      • MySQL
      • Nginx
      • Redis
      • Windows
    • 资源类
      • 论文
      • 书籍推荐
      • 后端资源
      • 前端资源
      • html网页模板
      • 代码
    • 性能优化
    • 测试
  • 重大新闻
  • 人工智能
  • 开源项目
  • Vue2.0从零开始
  • 广场
首页 › Vue › 现代浏览器观察者 Observer API 指南(新)

现代浏览器观察者 Observer API 指南(新)

迦娜王
8月前Vue
129 0 0

前言

前段时间在研究前端异常监控/埋点平台的实现。

在思考方案时,想到了浏览器自带的观察者以及页面生命周期API 。

于是在翻查资料时意外发现,原来现代浏览器支持多达四种不同类型的观察者:

  • Intersection Observer,交叉观察者。
  • Mutation Observer,变动观察者。
  • Resize Observer,视图观察者。
  • Performance Observer,性能观察者
IntersectionObserver MutationObserver ResizeObserver PerformanceObserver
用途 观察一个元素是否在视窗可见 观察DOM中的变化 观察视口大小的变化 监测性能度量事件
方法 observe()
disconnect()
takeRecords()
observe()
disconnect()
takeRecords()
unobserve()
observe()
disconnect()
unobserve()
observe()
disconnect()
takeRecords()
取代 Dom Mutation events getBoundingClientRect() 返回元素的大小及其相对于可视窗口的位置

Scroll 和 Resize 事件

Resize 事件 Performance 接口
用途 1. 无限滚动
2. 图片懒加载
3. 兴趣埋点
4. 控制动画/视频执行(性能优化)
1. 更高性能的数据绑定及响应
2. 实现视觉差滚动
3. 图片预加载
4. 实现富文本编辑器
1. 更智能的响应式布局(取代@media)
2. 响应式组件
1. 更细颗粒的性能监控
2. 分析性能对业务的影响(交互快/慢是否会影响销量)

1. IntersectionObserver:交叉观察者

IntersectionObserver接口,提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法,祖先元素与视窗(viewport)被称为根(root)

1. 出现的意义

想要计算Web页面的元素的位置,非常依赖于DOM状态的显式查询。但这些查询是同步的,会导致昂贵的样式计算开销(重绘和回流),且不停轮询会导致大量的性能浪费。

于是便发展了以下的几种方案:

  • 构建DOM和数据的自定义预加载和延迟加载。
  • 实现了数据绑定的高性能滚动列表,该列表加载和呈现数据集的子集。
  • 通过scroll等事件或通过插件的形式,计算真实元素可见性。

而它们都有几项共同特点:

  1. 基本实现形式都是查询各个元素相对与某些元素(全局视口)的“被动查询”。
  2. 信息可以异步传递(例如从另一个线程传递),且没有统一捕获错误的处理。
  3. web平台支持匮乏,各有各家的处理。需要开发人员消耗大量精力兼容。

2. IntersectionObserver的优势

Intersection Observer API通过为开发人员提供一种新方法来异步查询元素相对于其他元素或全局视口的位置,从而解决了上述问题:

  • 异步处理消除了昂贵的DOM和样式查询,连续轮询以及使用自定义插件的需求。
  • 通过消除对这些方法的需求,可以使应用程序显着降低CPU,GPU和资源成本。

3. IntersectionObserver基本使用

使用IntersectionObserver API主要需要三个步骤:

  1. 创建观察者
  2. 定义回调事件
  3. 定义要观察的目标对象

1.创建观察者

const options = {
    root: document.querySelector('.scrollContainer'),
    rootMargin: '0px',
    threshold: [0.3, 0.5, 0.8, 1] }
    
const observer = new IntersectionObserver(handler, options)
复制代码

这几个参数用大白话解释就是:

  1. root:指定一个根元素
  2. rootMargin:使用类似于设置CSS边距的语法来指定根边距(根元素的观察影响范围)
  3. threshold:阈值,可以为数组。[0.3]意味着,当目标元素在根元素指定的元素内可见30%时,调用处理函数。

2. 定义回调事件

当目标元素与根元素通过阈值相交时,就会触发回调函数。

function handler (entries, observer) { 
    entries.forEach(entry => { 
    // 每个成员都是一个IntersectionObserverEntry对象。
    // 举例来说,如果同时有两个被观察的对象的可见性发生变化,entries数组就会有两个成员。
    // entry.boundingClientRect 
    // entry.intersectionRatio 
    // entry.intersectionRect 
    // entry.isIntersecting 
    // entry.rootBounds 
    // entry.target 
    // entry.time 
    }); 
}
复制代码
  • time 时间戳
  • rootBounds 根元素的位置
  • boundingClientRect 目标元素的位置信息
  • intersectionRect 交叉部分的位置信息
  • intersectionRatio 目标元素的可见比例,看下图示
  • target。

3. 定义要观察的目标对象

任何目标元素都可以通过调用.observer(target)方法来观察。

const target = document.querySelector(“.targetBox”); 
observer.observe(target);
复制代码

此外,还有两个方法:

停止对某目标的监听

observer.unobserve(target)
复制代码

终止对所有目标的监听

observer.disconnect()
复制代码

4. 例子1:图片懒加载

HTML:

<img src="placeholder.png" data-src="img-1.jpg">
<img src="placeholder.png" data-src="img-2.jpg">
<img src="placeholder.png" data-src="img-3.jpg">
<!-- more images -->
复制代码

脚本:

let observer = new IntersectionObserver(
(entries, observer) => { 
entries.forEach(entry => {
    /* 替换属性 */
    entry.target.src = entry.target.dataset.src;
    observer.unobserve(entry.target);
  });
}, 
{rootMargin: "0px 0px -200px 0px"});

document.querySelectorAll('img').forEach(img => { observer.observe(img) });
复制代码

上述例子表示 仅在到达视口距离底部200px视加载图片。

5. 例子2:兴趣埋点

关于兴趣埋点,一个比较通用的方案是:

来自:《超好用的API之IntersectionObserver》

const boxList = [...document.querySelectorAll('.box')]

var io = new IntersectionObserver((entries) =>{
  entries.forEach(item => {
    // intersectionRatio === 1说明该元素完全暴露出来,符合业务需求
    if (item.intersectionRatio === 1) {
      // 。。。 埋点曝光代码
      io.unobserve(item.target)
    }
  })
}, {
  root: null,
  threshold: 1, // 阀值设为1,当只有比例达到1时才触发回调函数
})

// observe遍历监听所有box节点
boxList.forEach(box => io.observe(box))
复制代码

至于怎样评断用户是否感兴趣,记录方式就见仁见智了:

  • 位于屏幕中间,并停留时长大于2秒,计数一次。
  • 区域悬停,触发定时器记录时间。
  • PC端记录鼠标点击次数/悬停时间,移动端记录touch事件

这里就不展开写了(我懒)。

6. 控制动画/视频 执行

这里提供控制视频的版本

HTML:

<video src="OSRO-animation.mp4" controls=""></video>
复制代码

js:

let video = document.querySelector('video');
let isPaused = false; /* Flag for auto-paused video */
let observer = new IntersectionObserver((entries, observer) => { 
  entries.forEach(entry => {
    if(entry.intersectionRatio!=1  && !video.paused){
      video.pause(); isPaused = true;
    }
    else if(isPaused) {video.play(); isPaused=false}
  });
}, {threshold: 1});
observer.observe(video);
复制代码

效果:

2. Mutation Observer:变动观察者

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

1. 出现的意义

归根究底,是MutationEvents的功能不尽人意:

  1. 在MDN中也写到了,是被DOM Event承认在API上有缺陷,反对使用。
  2. 核心缺陷是:性能问题和跨浏览器支持。
  3. 为DOM添加 mutation 监听器极度降低进一步修改DOM文档的性能(慢1.5 – 7倍),此外, 移除监听器不会逆转的损害。

来自:《监听DOM加载完成及改变——MutationObserver应用》

MutationEvents的原理:通过绑定事件监听DOM

乍一看到感觉很正常,那列一下相关监听的事件:

DOMAttributeNameChanged
DOMCharacterDataModified
DOMElementNameChanged
DOMNodeInserted
DOMNodeInsertedIntoDocument
DOMNodeRemoved
DOMNodeRemovedFromDocument
DOMSubtreeModified
复制代码

甭记,这么多事件,各内核各版本浏览器想兼容怕是要天荒地老。

2. MutationObserver的优势

而Mutation Observer的优势在于:

  • MutationEvents事件是同步触发,也就是说,DOM 的变动立刻会触发相应的事件;
  • Mutation Observer 则是异步触发,DOM 的变动并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发。
  • 可以通过配置项,监听目标DOM下子元素的变更记录

简单讲:异步万岁!

3. MutationObserver基本使用

使用MutationObserver API主要需要三个步骤:

  1. 创建观察者
  2. 定义回调函数
  3. 定义要观察的目标对象

1. 创建观察者

let observer = new MutationObserver(callback);

复制代码

2. 定义回调函数

上面代码中的回调函数,会在每次 DOM 变动后调用。该回调函数接受两个参数,第一个是变动数组,第二个是观察器实例,下面是一个例子:

function callback (mutations, observer) {
  mutations.forEach(function(mutation) {
    console.log(mutation);
  });
});
复制代码

其中每个mutation都对应一个MutationRecord对象,记录着DOM每次发生变化的变动记录

MutationRecord对象包含了DOM的相关信息,有如下属性:

属性 意义
type 观察的变动类型(attribute、characterData或者childList)
target 发生变动的DOM节点
addedNodes 新增的DOM节点
removedNodes 删除的DOM节点
previousSibling 前一个同级节点,如果没有则返回null
nextSibling 下一个同级节点,如果没有则返回null
attributeName 发生变动的属性。如果设置了attributeFilter,则只返回预先指定的属性
oldValue 变动前的值。这个属性只对attribute和characterData变动有效,如果发生childList变动,则返回null

3. 定义要观察的目标对象

MutationObserver.observe(dom, options)
复制代码

启动监听,接收两个参数。

  • 第一参数:被观察的DOM节点。
  • 第二参数:配置需要观察的变动项options。
mutationObserver.observe(content, {
    attributes: true, // Boolean - 观察目标属性的改变
    characterData: true, // Boolean - 观察目标数据的改变(改变前的数据/值)
    childList: true, // Boolean - 观察目标子节点的变化,比如添加或者删除目标子节点,不包括修改子节点以及子节点后代的变化
    subtree: true, // Boolean - 目标以及目标的后代改变都会观察
    attributeOldValue: true, // Boolean - 表示需要记录改变前的目标属性值
    characterDataOldValue: true, // Boolean - 设置了characterDataOldValue可以省略characterData设置
    // attributeFilter: ['src', 'class'] // Array - 观察指定属性
});
复制代码

优先级 :

  1. attributeFilter/attributeOldValue > attributes
  2. characterDataOldValue > characterData
  3. attributes/characterData/childList(或更高级特定项)至少有一项为true;
  4. 特定项存在, 对应选项可以忽略或必须为true

此外,还有两个方法:

停止观察。调用后不再触发观察器,解除订阅

MutationObserver.disconnect()
复制代码

清除变动记录。即不再处理未处理的变动。该方法返回变动记录的数组,注意,该方法立即生效。

MutationObserver.takeRecords()
复制代码

4. 例子1:MutationObserver监听文本变化

基本使用是:

const target = document.getElementById('target-id')

const observer = new MutationObserver(records => {
  // 输入变更记录
})

// 开始观察
observer.observe(target, {
  characterData: true
})
复制代码

这里可以有几种处理。

  • 聊天的气泡框彩蛋,检测文本中的指定字符串/表情包,触发类似微信聊天的表情落下动画。
  • 输入框的热点话题搜索,当输入“#”号时,启动搜索框预检文本或高亮话题。

有个Vue的小型插件就是这么实现的:

来自:《vue-hashtag-textarea》

5. 例子2: 色块小游戏脚本

这个实现也是秀得飞起:

Hacking the color picker game — MutationObserver

游戏的逻辑很简单,当中间的色块颜色改变时,在时间限制内于底下的选项选择跟它颜色一样的选项就得分。难的点在于越后面的关卡选项越多,而且选项颜色也越相近,例如:

其实原理非常简单,就是观察色块的backgroundColor(属性变化attributes),然后触发点击事件e.click()。

var targetNode = document.querySelector('#kolor-kolor');
var config = { attributes: true };
var callback = function(mutationsList, observer) {
    if (mutationsList[0].type == 'attributes') {
        console.log('attribute change!');
        let ans = document.querySelector('#kolor-kolor').style.backgroundColor;
        document.querySelectorAll('#kolor-options a').forEach( (e) => {
            if (e.style.backgroundColor == ans) {
                e.text = 'Ans!';
                e.click()
            }
        })
    }
};

var observer = new MutationObserver(callback);
observer.observe(targetNode, config);
复制代码

3. ResizeObserver,视图观察者

ResizeObserver API是一个新的JavaScript API,与IntersectionObserver API非常相似,它们都允许我们去监听某个元素的变化。

1. 出现的意义

  • 开发过程当中经常遇到的一个问题就是如何监听一个 div 的尺寸变化。

  • 但众所周知,为了监听 div 的尺寸变化,都将侦听器附加到 window 中的 resize 事件。

  • 但这很容易导致性能问题,因为大量的触发事件。

  • 换句话说,使用
    window.resize 通常是浪费的,因为它告诉我们每个视窗大小的变化,而不仅仅是当一个元素的大小发生变化。

  • 而且resize事件会在一秒内触发将近60次,很容易在改变窗口大小时导致性能问题

比如说,你要调整一个元素的大小,那就需要在 resize 的回调函数 callback() 中调用 getBoundingClientRect 或 getComputerStyle。不过你要是不小心处理所有的读和写操作,就会导致布局混乱。比如下面这个小示例:

2. ResizeObserver的优势

ResizeObserver API 的核心优势有两点:

  • 细颗粒度的DOM元素观察,而不是window
  • 没有额外的性能开销,只会在绘制前或布局后触发调用

3. ResizeObserver基本使用

使用ResizeObserver API同样也是三个步骤:

  1. 创建观察者
  2. 定义回调函数
  3. 定义要观察的目标对象

1. 创建观察者

let observer = new ResizeObserver(callback);

复制代码

2. 定义回调函数

const callback = entries => {
    entries.forEach(entry => {
        
    })
}
复制代码

每一个entry都是一个对象,包含两个属性contentRect和target

contentRect都是一些位置信息:

属性 作用
bottom top + height的值
height 元素本身的高度,不包含padding,border值
left padding-left的值
right left + width的值
top padidng-top的值
width 元素本身的宽度,不包含padding,border值
x 大小与top相同
y 大小与left相同

3. 定义要观察的目标对象

observer.observe(document.body)
复制代码

unobserve方法:取消单节点观察

observer.unobserve(document.body)
复制代码

disconnect方法:取消所有节点观察

observer.disconnect(document.body)
复制代码

4. 例子1:缩放渐变背景

html:

<div class="box">
    <h3 class="info"></h3>
</div>
<div class="box small">
    <h3 class="info"></h3>
</div>
复制代码

添加点样式:

body {
    width: 100vw;
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 2vw;
    box-sizing: border-box;
}
.box {
    text-align: center;
    height: 20vh;
    border-radius: 8px;
    box-shadow: 0 0 4px rgba(0,0,0,.25);
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 1vw
}
.box h3 {
    color: #fff;
    margin: 0;
    font-size: 5vmin;
    text-shadow: 0 0 10px rgba(0,0,0,0.4);
}
.box.small {
    max-width: 550px;
    margin: 1rem auto;
}
复制代码

JavaScript代码:

const boxes = document.querySelectorAll('.box');
let callbackFired = 0;
const myObserver = new ResizeObserver(entries => {
    for (let entry of entries) {
        callbackFired++
        const infoEl = entry.target.querySelector('.info');
        const width = Math.floor(entry.contentRect.width);
        const height = Math.floor(entry.contentRect.height);
        const angle = Math.floor(width / 360 * 100);
        const gradient = `linear-gradient(${ angle }deg, rgba(0,143,104,1) 50%, rgba(250,224,66,1) 50%)`;
        entry.target.style.background = gradient;
        infoEl.innerText = `
        I'm ${ width }px and ${ height }px tall
        Callback fired: ${callbackFired}
        `;
    }
});
boxes.forEach(box => {
    myObserver.observe(box);
});
复制代码

当你拖动浏览器窗口,改变其大小时,看到的效果如下:

5. 例子2:响应式Vue组件

  • 假设你要创建一个postItem组件,在大屏上是这样的显示效果

  • 在手机上需要这样的效果:

简单的@media就可以实现:

@media only screen and (max-width: 576px) {
  .post__item {
    flex-direction: column;
  }
  
  .post__image {
    flex: 0 auto;
    height: auto;
  }
}
复制代码
  • 但这就很容易出现 当你在超过预期的屏幕(过大)查看页面时,会出现以下的布局:

@media查询的最大问题是:

  • 组件响应度取决于屏幕尺寸,而不是响应自身的尺寸。

以下是指令版实现:

使用:

效果:

这是vue-responsive-components库的具体实现代码,还有组件形式的实现,感兴趣的可以去看看。

4. PerformanceObserver:性能观察者

这是一个浏览器和Node.js 里都存在的API,采用相同W3C的Performance Timeline规范

  • 在浏览器中,我们可以使用 window 对象取得window.performance和 window.PerformanceObserver 。
  • 而在 Node.js 程序中需要perf_hooks 取得性能对象,如下:
    const { PerformanceObserver, performance } = require('perf_hooks');
    复制代码

1. 出现的意义

首先来看Performance 接口:

  • 可以获取到当前页面中与性能相关的信息。它是 High Resolution Time API 的一部分,同时也融合了 Performance Timeline API、Navigation Timing AP、 User Timing API 和 Resource Timing API。

  • Performance API 是大家熟悉的一个接口,他记录着几种性能指数的庞大对象集合。

0
技术胖155集前端视频教程-全部免费观看
上一篇
前端面经题记:长列表怎么优化?
下一篇
评论 (0)

请登录以参与评论。

现在登录
聚合文章
在Gitee收获近 5k Star,更新后的Vue版RuoYi有哪些新变化?
2月前
vue3.x reactive、effect、computed、watch依赖关系及实现原理
2月前
Vue 3 新特性:在 Composition API 中使用 CSS Modules
2月前
新手必看的前端项目去中心化和模块化思想
2月前
标签
AI AI项目 css docker Drone Elaticsearch es5 es6 Geometry Go gru java Javascript jenkins lstm mysql mysql优化 mysql地理位置索引 mysql索引 mysql规范 mysql设计 mysql配置文件 mysql面试题 mysql高可用 nginx Redis redis性能 rnn SpringBoot Tensorflow tensorflow2.0 UI设计 vue vue3.0 vue原理 whistle ZooKeeper 开源项目 抓包工具 日志输出 机器学习 深度学习 神经网络 论文 面试题
相关文章
在Gitee收获近 5k Star,更新后的Vue版RuoYi有哪些新变化?
vue3.x reactive、effect、computed、watch依赖关系及实现原理
Vue 3 新特性:在 Composition API 中使用 CSS Modules
新手必看的前端项目去中心化和模块化思想
松鼠乐园

资源整合,创造价值

小伙伴
墨魇博客 无同创意
目录
重大新闻 Centos CSS Docker ES5 ES6 Go Java Javascript Linux Mac MySQL Nginx Redis Springboot Tensorflow Vue Vue2.x从零开始 Windows 书籍推荐 人工智能 前端资源 后端资源 壁纸 开源项目 测试 论文
Copyright © 2018-2021 松鼠乐园. Designed by nicetheme. 浙ICP备15039601号-4
  • 重大新闻
  • Centos
  • CSS
  • Docker
  • ES5
  • ES6
  • Go
  • Java
  • Javascript
  • Linux
  • Mac
  • MySQL
  • Nginx
  • Redis
  • Springboot
  • Tensorflow
  • Vue
  • Vue2.x从零开始
  • Windows
  • 书籍推荐
  • 人工智能
  • 前端资源
  • 后端资源
  • 壁纸
  • 开源项目
  • 测试
  • 论文
热门搜索
  • jetson nano
  • vue
  • java
  • mysql
  • 人工智能
  • 人脸识别
迦娜王
坚持才有希望
1224 文章
33 评论
233 喜欢
  • 0
  • 0
  • Top