React
react 合成事件
先来了解下浏览器事件
了解下事件代理
事件代理的优势
-
只要定义一个监听函数,就能处理多个子节点的事件,而不用在每个
子节点上定义监听函数。 -
而且以后再添加子节点,监听函数依然有效。 事件代理的劣势
-
委托会导致浏览器频繁调用处理函数,虽然很可能不需要处理。所以建议就近委托,比如在table上代理td,而不是在document上代理td。 由于运行 Javascript 是主线程的工作,当页面被合成线程合成过,合成线程会标记那些有事件监听的区域。有了这些信息,当事件发生在响应的区域时,合成线程就会将事件发送给主线程处理。如果在非事件监听区域,则渲染进程直接创建新的帧而不关心主线程。通过事件代理,可以更高效的监听事件。但如果从浏览器的角度看,此时整个页面会被标记成“慢滚动”区域。这意味着虽然页面中的某些部分并不需要事件监听,但合成线程依然要在每次交互发生后等待主线程处理事件,合成线程的优化效果不复存在。 可在事件代理时传入
passive: true(IE 不支持) 参数。这样告诉渲染线程,依然需要将事件发送给主线程处理,但不需要等待。 -
把所有事件都用代理就可能会出现事件误判。比如,在document中代理了所有button的click事件,另外的人在引用改js时,可能不知道,造成单击button触发了两个click事件。
从浏览器的角度看事件
当我们听到事件时,通常会联想到在一个文本框中输入或者单击鼠标,但从浏览器的角度看,输入事件意味着所有的用户动作。鼠标滚轮滚动或者屏幕触摸都是输入事件。
当用户与页面发生交互时,浏览器进程首先接收到事件,然而,浏览器进程只关心事件发生时是在哪个页签中,所以浏览器进程会将事件类型和位置信息等发送给负责当前页签的渲染进程,渲染进程会恰当的找到事件发生的元素并且触发事件监听器。
input (1).png
合成线程对事件的处理
在前面的章节中,我们知道了合成线程可以通过合成技术合成不同的光栅层优化性能,如果页面并不监听任何事件,合成线程可以完全独立于主线程生成新的合成帧。但如果页面监听了事件呢?
标记“慢滚动”区域
由于运行 Javascript 是主线程的工作,当页面被合成线程合成过,合成线程会标记那些有事件监听的区域。有了这些信息,当事件发生在响应的区域时,合成线程就会将事件发送给主线程处理。如果在非事件监听区域,则渲染进程直接创建新的帧而不关心主线程。
在事件监听时标记
在 web 开发中常见的方式就是事件代理。利用事件冒泡,我们可以在目标元素的上层元素中监听事件。参照下面的代码。
document.body.addEventListener('touchstart', event => { if (event.target === area) { event.preventDefault(); }});
通过这种写法,可以更高效的监听事件。但如果从浏览器的角度看,此时整个页面会被标记成“慢滚动”区域。这意味着虽然页面中的某些部分并不需要事件监听,但合成线程依然要在每次交互发生后等待主线程处理事件,合成线程的优化效果不复存在。
为了解决这个问题,我们可在事件代理时传入passive: true (IE 不支持) 参数。这样告诉渲染线程,依然需要将事件发送给主线程处理,但不需要等待。
document.body.addEventListener('touchstart', event => { if (event.target === area) { event.preventDefault() } }, {passive: true});
关于使用 passive 改善滚屏性能,可以参考MDN 使用passive改善滚屏性能。
查找事件目标
当渲染线程将事件发送给主线程后,第一件事就是找到事件触发的目标。通过在渲染过程中生成的绘制信息,可以根据坐标找到目标元素。
减少发送给主线程的事件数量
为了保证动画的顺畅,需要显示器在每秒刷新 60 次。对于典型的触摸事件由合成线程提交给主线程的事件频率可以达到每秒 60-120 次,对于典型的鼠标事件每秒会发送 100 次。事件发送的频率通常比屏幕刷新频率要高。
如果类似touchmove这样的事件每秒向主线程发送 120 次可能会造成主线程执行时间过长而影响性能。
为了减少发送给主线程的事件数量,Chrome 合并了连续的事件。类似wheel,mousewheel,mousemove,pointermove,touchmove这样的事件会被延迟到下一次requestAnimationFrame前触发.
而任何的离散事件,类似keydown, keyup, mouseup, mousedown, touchstart和 touchend都会立即被发送给主线程处理。
react合成事件
React 实现了一个合成事件层*,就是这个事件层,把 IE 和 W3C 标准之间的兼容问题给消除了***
react16和17事件运行
import * as React from 'react';
import * as ReactDOM from 'react-dom';
class App extends React.Component {
parentRef=React.createRef();
childRef=React.createRef();
componentDidMount() {
this.parentRef.current.addEventListener("click", () => { console.log("父元素原生捕获"); },true);
this.parentRef.current.addEventListener("click", () => { console.log("父元素原生冒泡"); });
this.childRef.current.addEventListener("click", () => { console.log("子元素原生捕获"); },true);
this.childRef.current.addEventListener("click", () => { console.log("子元素原生冒泡"); });
document.addEventListener('click',()=>{ console.log("document捕获"); },true);
// 注册是在react 应用之后注册的事件函数 dispatchEvent 执行合成事件回调
// react 会添加一个 document.addEventListener('click', dispatchEvent);
// dispatchEvent 会模拟一遍捕获阶段,目标阶段和 冒泡阶段
// dispatchEvent 大致收集 触发的current,并找到current的所有父节点 依次找到此节点和父节点上找到对应fiber-jsx上面的 onXXXcaptch , onXXX 并按照先捕获再冒泡进行执行
document.addEventListener('click',()=>{ console.log("document冒泡"); });
}
parentBubble = () => { console.log("父元素React事件冒泡"); };
childBubble = () => { console.log("子元素React事件冒泡"); };
parentCapture = () => { console.log("父元素React事件捕获"); };
childCapture = () => { console.log("子元素React事件捕获"); };
render() {
return (
<div ref={this.parentRef} onClick={this.parentBubble} onClickCapture={this.parentCapture}>
<p ref={this.childRef} onClick={this.childBubble} onClickCapture={this.childCapture}>
事件执行顺序
</p>
</div> );
} }
ReactDOM.render(<App />, document.getElementById('root'));
/**
react17之前
document捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
父元素React事件捕获
子元素React事件捕获
子元素React事件冒泡
父元素React事件冒泡
document冒泡
react 17及之后 对root进行代理,并且是root 捕获阶段先处理捕获的事件回调,等到冒泡了,在处理冒泡的事件回调
父元素React事件捕获
子元素React事件捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
子元素React事件冒泡
父元素React事件冒泡
*/
也是用的代理,v17之前代理在document上,v17及之后代理到当前react应用的挂载点 v17代理到document上会过于扩大范围,比如一个react应用,只是想监听一种元素下 一种事件,但是如果代理到了document上 ,如果页面上有另一个react应用,那么有可能会发生互相干扰,监听到当前应用的挂载点,可以很有效的减少react应用之间的互相干扰
如果页面上有多个 React 版本,他们都将在顶层注册事件处理器。这会破坏 e.stopPropagation():如果嵌套树结构中阻止了事件冒泡,但外部树依然能接收到它。这会使不同版本 React 嵌套变得困难重重
上面的讲解一下, 一个react应用是一个fiber链表数结构,如果在顶层document进行监听,如果此事件里面用了stopProgagation/stopImmediatePropagation,那么事件上这个冒泡还是会冒泡到document上的,只是在此react应用中控制的dom/fiber链表上的dom会被当前react应用控制而不会发生冒泡,但是其他的非此react应用控制的dom则还是会监听到该事件,违背了想要stopProgagation/stopImmediatePropagation 的目标
所以v17把应用挂载到了 当前react挂载的节点,这样就避免了干扰的问题
此更改还使得将 React 嵌入使用其他技术构建的应用程序变得更加容易。例如,如果应用程序的“外壳”是用 jQuery 编写的,但其中较新的代码是用 React 编写的,则 React 代码中的 e.stopPropagation() 会阻止它影响 jQuery 的代码 —— 这符合预期。换个角度来说,如果你不再喜欢 React 并想重写应用程序(比如,用 jQuery),则可以从外壳开始将 React 转换为 jQuery,而不会破坏事件冒泡。
(从 v17 开始,e.persist() 将不再生效,因为 SyntheticEvent 不再放入事件池中。) v17 去除事件池了
因为合成事件的触发是基于浏览器的事件机制来实现的,通过冒泡机制冒泡到最顶层元素,然后再由 dispatchEvent 统一去处理
合成事件特点
React 自己实现了这么一套事件机制,它在 DOM 事件体系基础上做了改进,减少了内存的消耗,并且最大程度上解决了 IE 等浏览器的不兼容问题
那它有什么特点?
- React 上注册的事件最终会绑定在
react挂载点这个 DOM 上(减少内存开销就是因为所有的事件都绑定在 react挂载点 上,其他节点没有绑定事件) - React 自身实现了一套事件冒泡机制,所以这也就是为什么我们
event.stopPropagation()无效的原因。 - React 通过队列的形式,从触发的组件向父组件回溯,然后调用他们 JSX 中定义的 callback
- React 有一套自己的合成事件
SyntheticEvent,抹平不同浏览器的差异 - React 通过对象池的形式管理合成事件对象的创建和销毁,减少了垃圾的生成和新对象内存的分配,提高了性能
react事件组成
-
事件注册
-
事件绑定
-
事件触发
-
ReactEventListener:负责事件的注册。
-
ReactEventEmitter:负责事件的分发。
-
EventPluginHub:负责事件的存储及分发。
-
Plugin:根据不同的事件类型构造不同的合成事件。
事件注册
React 中注册一个事件:
class xxx extends Reac.PureComponent {
render() {
return (
<div
onClick={() => {
console.log('xxx')
}}
>
xxx
</div>
)
}
}
将原生事件和对应的onxxx captch 等react事件进行set集合中进行注册,也就是收集有哪些dom原生事件及这些原生事件所对应的react事件属性(onClickCaptch) 原生事件放在 allNativeEvent 的set集合中 --- 事件注册
事件绑定
- 区分事件,分为两种事件,一种是需要进行监听冒泡阶段的,一种是不需要监听冒泡阶段的(scroll,cancel,load等事情不需要监听冒泡)
- 同一个容器的同一个阶段的同一个事件只需要绑定一次
- 将allNativeEvent 中的所有事件都和 root节点进行绑定,部分事件不需要监听冒泡,监听函数进行处理, dispatchEvent.bind(null, domEventName, eventSystemFlags, rootContainerElement)
/**
domEventName 事件名字 click
eventSystemFlags 事件系统标识 0 4
rootContainerElement 目标荣耀 div#root
nativeEvent --- 事件真正触发时候原生事件对象 会被传入
*/
function dispatchEvent(domEventName, eventSystemFlags, rootContainerElement, nativeEvent) {
}
事件触发查找, 收集处理函数
fiber树大致属性 stateNode 对应fiber节点对应的原生dom,return属性对应器原生dom父节点dom
const internalInstanceKey = '__reactFiber$xxx'; // xxx 是一个随机数
const internalPropsKey = '__reactProps$xxx'; //
// 上方两个属性都是在fiber生成dom的时候挂载在dom上的属性, internalInstanceKey 指向此dom的 fiber节点对象, internalPropsKey 指向此fiber节点对象的props属性
/**
domEventName 事件名字 click
eventSystemFlags 事件系统标识 0 4
rootContainerElement 目标荣耀 div#root
nativeEvent --- 事件真正触发时候原生事件对象 会被传入
*/
function dispatchEvent(domEventName, eventSystemFlags, rootContainerElement, nativeEvent) {
// 获取原生事件源
const target = nativeEvent.target || nativeEvent.srcElement || window;
// 获取fiber对象
const targetIns = target['internalInstanceKey'];
// 获取props
const targetIns = target['internalPropsKey'];
// 交给plugin进行处理
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer
);
}