自动收集

291 阅读8分钟

0.前置知识

0.1 DOM事件注册

DOM中注册事件的三种方法
1.【在HTML元素上提供的事件属性】,onclick(不是react合成事件中的驼峰),即通过</div onclick=function>
2.【DOM对象上的事件属性】先通过getElementById获取dom元素,然后通过DOM.onclick = function来绑定回调事件。
3.【DOM规范提供的addEvent.addEventListener】,使用该API可以实现一个dom上绑定多个点击事件。

0.2 DOM事件流

当触发某个dom元素的时候,这个事件不是一下子到达了目标元素,而是通过按照一定的顺序在DOM树中进行传播,进而完成整个过程的,这个顺序就是DOM事件流:

  1. 事件捕获阶段
  2. 目标对象本身的事件处理程序调用
  3. 事件冒泡

0.2 影响DOM事件的API(3个)

  • event.preventDefault:阻止默认行为。对于a标签或者form表单元素在处理点击事件的时候除了会执行点击回调函数,此外还有一些默认行为:例如a标签点击之后,处理完click事件还会有跳转事件,这个跳转事件就是a标签的默认行为。这种默认行为我们可以在click事件回调中通过preventDefault来阻止执行。
  • event.stopPropagation: 阻止当前元素向上冒泡。子元素可以阻止点击事件冒泡到父元素
  • event.stopImmediatePropagation:阻止当前元素冒泡 && 阻止当前元素自身之后绑定的其他addEvenetListener('click', function)。
    例如一个dom元素通过addEvenetListener在click事件上绑定了好几个回调函数,有先后顺序之分,那么通过stopImmediatePropagation也可以阻止后面绑定的回调的执行。

0.3 react合成事件

react为了区分自身框架的合成事件和原生事件,在事件的命名上采用了驼峰onClick方式(不同于原生的onclick),这样在渲染JSX的时候,查看dom上的props就知道这个onClick是框架自身的“合成事件”。 react17之前是在document上绑定监听,后面是在root上监听合成事件

(1) react合成事件和原生事件的执行顺序

当我们在react框架中,通过【addEventListenr 绑定原生事件】,又在JSX中通过【onClick 绑定合成事件】,那么同一个元素的绑定

相关问题
1.react为什么不在事件捕获中处理
从功能上讲事件捕获和事件冒泡没有明显的差别,但是在兼容性方面,事件冒泡已经被主流浏览器兼容,但是事件捕获的兼容性就相对较差一些,例如在IE9以下的浏览器就不能兼容事件捕获。因此react合成事件采用了事件冒泡来完成。

1.背景

问题现状:埋点代码和业务代码耦合到了点击回调函数中。 - 正常的埋点需要在click点击回调的时候加上埋点的的发送请求,这就将埋点的逻辑和业务的逻辑耦合到了一起。

2. Click 埋点事件

2.1 主要矛盾 && 解决目标

我们希望
(1)像html/JS/CSS隔离原则那样,在html-DOM上注册埋点cid:即在html元素上标注埋点标识符cid属性=cid属性值
(2)然后借鉴react合成事件中“事件代理”的思想,在最外层包装一个埋点SDK来处理【埋点搜集】和【埋点上报】两个操作 通过这两点来实现埋点代码的抽离。

2.2 核心思路

通过DOM事件流在document层注册监听去解决

所以我们借鉴react合成事件中“事件代理”的思想,在最外层包装一个埋点SDK来处理埋点逻辑。 也就是说我们可以在冒泡阶段去拿到对应的eventTarg,然后我们就可以拿到目标dom,然后去查找目标dom上的cid属性,然后发送上报请求。

2.3 相关问题

【问题1】如何解决stopPropagation阻止冒泡的行为

react中使用事件冒泡,他是允许用户通过stopPropagation来阻止后续冒泡行为的。但是我们这里并不希望埋点的上报受到用户stoPropagation的影响。所以把事件流的监听改在了事件捕获节点执行(addeventListener的第三个参数为true)。
【为什么react不采用事件捕获处理?】 这里额外提一下,事件捕获和事件冒泡在功能上没有很大区别,但是兼容性上,主流浏览器更多的是兼容事件冒泡,这也是为什么react采用事件冒泡来实现合成事件。

【问题2】如何解决第三方组件上的上报

问题表现:我们是通过 event.target元素上去查找data-lx属性 ,但是无法在三方组件的target元素上注册。
问题原因:因为我是在事件捕获,处于事件阶段,拿到eventTarge对应的dom元素,然后解析dom对应的cid属性拿到的点位信息,但是在使用UI组件的时候,eventTarge一定是组件内部的元素,而我们的cid最多注册在组件的外层,没办法通过eventTarget来拿到。
解决方案:在事件触发的时候,通过递归查找,即先在eventTarge中查找是否存在data-cid属性,如果没有的话,那么从targetEvenet开始向上找他的直接parentNode父节点,查看有没有data-cid属性,直到找到document层为止,如果没有则认为这个节点没有绑定埋点。

【问题3】如果将这个子应用注册到微前端中,会有什么问题?

问题表现:一个点击事件发送多次重复埋点请求

出现原因子应用中拿的document是主应用的 && qinakun加载子应用原理
主应用的document被重复注册了。因为在qiankun中子应用拿到的document并不是子应用自己的document,而是主应用的document。因为在一个页面中只能有一个HTML标签,否则就会报错。
所以qiankun只能通过fetch请求拿到的子应用的html资源,然后通过import-html-entry进行拆解得到对应的JS/CSS资源,并创建动态script标签放在主应用的HTML中。
因此子应用拿到的是主应用的document,这就导致了多个子应用重复注册了document事件,进而导致发送重复请求

【解决方案1】:我们在子应用卸载的时候,通过removeEventListener来移除埋点事件。(缺点:如果一个页面一次性要展示两个子应用,依然会导致重复注册);
【解决方案2】:参考react17之后对合成事件的改进,将注册点放在对应子应用的根节点上,这样就互相不影响;
【解决方案3】:获取document的注册事件,我可以通过document.getEventListeners(document)获取注册的事件,然后通过对比已经注册的回调函数是否和将要注册的事件回调相同;
【解决方案4】:通过在window上注册一个标志位来判断,是否已经注册了埋点搜集SDK,但是由于qiankun中的【JS隔离策略】,导致子应用无法共享window,因此这个方案也不行。

3. 组件曝光埋点事件

3.1 主要矛盾

页面中监听的元素过多产生的性能问题

我们通常在实现无线滚动 && 埋点曝光等场景下,需要监听判断元素和视口的位置关系,进而确定是否在视口中。而我们常用的方案就是通过scroll监听,然后通过地方querySelectorAll('data-lx-**') 获取元素的位置和视口判断。

但是scroll事件的触发非常密集就会导致计算量非常大,虽然可以通过节流来缓解一下性能问题,但是最后还是要通过getElement获取位置信息,而这些js的执行最后还是要放在主任务中执行,也算是同步执行。

3.2 解决方案 - Intersection-observe

因此浏览器提供了一个新的方案,专门用来解决这种场景问题,交叉观察-API:intersection-observer-API。这里的交叉就是指两个元素是否存在交叉,通过这个API注册两个dom元素,浏览器会自动判断这两个dom是否存在交叉-重合,而不用我们再去通过位置计算是否重合。

这里省去了我们通过位置计算重合的步骤,我们只需要通过qeurySelectorAll获取的两个dom注册好,然后当两个dom重合的时候会自动触发这个API的回调函数:

// 创建一个 Intersection Observer实例
const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) { // entries是数组,里面是每一个注册的子元素,如果某一个子元素与父元素发生了重合,那么item中的重合属性就会置为true。 
            // 当元素进入视口时执行的代码
            console.log('元素进入视口', entry.target);
            entry.target.classList.add('visible');
            // 如果只需要观察一次,可以调用 unobserve 方法停止观察
            observer.unobserve(entry.target);
        }
    });
}, {
    root: null, // 这里放的是父元素,如果为null则默认子元素交叉的对象是视口
    rootMargin: '0px', // 可选,视口边缘的偏移量
    threshold: 0.5 // 可选,元素与视口交叉的阈值,这里是指子元素的身体有50%进入到父元素中则认为发生了交叉
});

// 选择要观察的子元素元素
const targetElements = document.querySelectorAll('.observe');
targetElements.forEach(element => {
    observer.observe(element); // 开始观察每个子元素元素,和intersection中注册的父元素-视口是否发生交叉
});

4.统计PV、UV数据

【问题4】如何统计pv,uv

一个有滚动条的页面内,曝光组件上下滚动几次都算曝光一次吗
介绍一下新的API: intersection Observer