文章借鉴 pingan8787 React合成事件 和 React合成事件官方文档

React 合成事件

一、概念介绍

React合成事件是React 模拟原生DOM事件所有能力 的一个事件对象。
根据 W3C规范 来定义合成事件,兼容所有浏览器,拥有与浏览器原声事件相同的接口。

合成事件除了拥有和浏览器原生事件相同的接口,包括stopPropagation()preventDefault()

⚠️ 在React中,所有事件都是合成的,不是原生DOM事件,可以通过 e.nativeEvent 属性获取原生DOM事件。合成事件不会映射到原生事件;例如:在onMouseLeave事件中event.nativeEvent 将指向mouseout 事件;

const handleClick = (e) => console.log(e.nativeEvent);;
const button = <button onClick={handleClick}>Leo 按钮</button>

二、为什么出现这个技术?

  • 浏览器兼容,实现更好的跨平台
    顶层事件代理机制:保证冒泡一致性,可以跨浏览器执行。将不同平台事件模拟成合成事件;

  • 避免垃圾回收
    React引入事件池,在事件池中获取或释放事件对象;
    React事件对象不会被释放掉,而是存入一个数组中;当事件触发,就从这个数组中弹出,避免频繁地创建和销毁(垃圾回收);

  • 方便事件统一管理和事务机制

三、原生事件回顾

  • 事件捕获:当某个元素触发某个事件(比如:onClick),顶层对象(document)就会发出一个事件流,随着DOM树的节点向目标节点流去,直到到达真正发生的目标元素;
  • 事件目标:执行处理函数
  • 事件冒泡:从目标元素开始,往顶层元素传播;途中有节点绑定了相应的事件处理函数,这些函数就会被触发一次。⚠️ 想要阻止事件冒泡,可以使用e.stopPropagation() 或者e.cancleBubble=true (IE)来阻止事件等等冒泡传播。
  • 事件委托 / 事件代理
    将一个响应事件委托到另一个元素;子节点被点击,click事件向上冒泡,父节点捕获到事件后,进行处理;(可以减少内存消耗和动态绑定事件

四、合成事件和原生事件的区别

  • 命名方式不同
    原生:onclick(纯小写)
    React:onClick(小驼峰)
  • 事件处理函数写法不同
    原生事件处理函数为字符串,React JSX语法中,传入一个函数作为事件处理函数
// 原生事件 事件处理函数写法
<button onclick="handleClick()">Leo 按钮命名</button>
// React 合成事件 事件处理函数写法
const button = <button onClick={handleClick}>Leo 按钮命名</button>
  • 阻止默认行为方式的不同
    原生事件:通过返回false 方式阻止默认行为;
    React:显式使用preventDefault() 方法阻止;比如阻止<a>标签默认打开新页面为例;
// 原生事件阻止默认行为方式
<a href="https://www.pingan8787.com" 
  onclick="console.log('Leo 阻止原生事件~'); return false"
>
  Leo 阻止原生事件
</a>

// React 事件阻止默认行为方式
const handleClick = e => {
  e.preventDefault();
  console.log('Leo 阻止原生事件~');
}
const clickElement = <a href="https://www.pingan8787.com" onClick={handleClick}>
  Leo 阻止原生事件
</a>
  • 事件处理器返回false时,不阻止事件传递;可以考虑调用e.stopPropagation()e.preventDefault()
  • 支持的事件:
    如果需要注册捕获阶段的事件处理函数,应该为事件名添加Capture ;例如:处理捕获阶段的点击事件请使用 onClickCapture 而不是 onClick

五、React事件和原生事件的执行顺序

合成事件的执行顺序
在React中,“合成事件” 会以事件委托的方式绑定到组件最上层,并在组件卸载阶段自动销毁绑定的事件。

  • React 所有事件都挂载在 document 对象上;
  • 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件;
  • 所以会先执行原生事件,然后处理 React 事件;
  • 最后真正执行 document 上挂载的事件。

原生事件 —— > React事件 —— > document事件

在页面上点击按钮,事件开始在原生DOM上走捕获冒泡流程,React监听的是document上的冒泡阶段;事件冒泡到document后,React将事件再派发到组件树中,然后事件开始在组件树DOM中走捕获冒泡流程。(React上监听的是document上的事件)

同一元素如果对同一类型的事件绑定来多个处理器,会按绑定的顺序来执行;

React中阻止事件冒泡的问题:
  • 事件处理器:
    默认情况下,事件处理器在事件的冒泡阶段执行,默认下:document.addEventListener() 中的 useCapture 参数为 false;
<button onclick="btnClickHandler(event)">CLICK ME</button>
<script>
  document.addEventListener("click", function(event) {
    console.log("document clicked");
  }); // 
  function btnClickHandler(event) {
    console.log("btn clicked");
  }
</script>

以上全部都是原生代码,所以输出:(最后冒泡到document

btn clicked
document clicked
  • 阻止事件的冒泡
    调用事件身上的stopPropagation() 可阻止事件往上冒泡,可以实现想要的元素处理该事件,而其他元素接收不到。(原生事件如果执行了stopPropagation,所有元素的事件将无法冒泡到document,这样的话,所有的React事件都将无法被注册。)

同一元素上同一类型的事件(比如:click事件)绑定来多个事件处理器,本来处理器按绑定的先后顺序来执行,但是如果其中一个调用了stopImmediatePropagation,不但会阻止事件冒泡,还会组织这个元素后续其他事件处理器的执行。

<button onclick="btnClickHandler(event)">CLICK ME</button>
<script>
  document.addEventListener(
    "click",
    function(event) {
      console.log("document clicked");
    },
    false
  );

  function btnClickHandler(event) {
    event.stopPropagation();
    console.log("btn clicked");
  }
</script>

输出:

btn clicked
  • e.stopPropagation

e.stopPropagation() 只能阻止合成事件间冒泡,即下层的合成事件,不会冒泡到上层的合成事件。事件本身还都是在 document 上执行。所以最多只能阻止 document 事件不能再冒泡到 window 上。
(document事件不执行:原生——React事件(e.stopPropagation())——document事件)

  • e.nativeEvent.stopImmediatePropagation

在React中,一个组件只能绑定一个同类型的事件监听器,当重复定义时,后面的监听器会覆盖之前的;
事实上 nativeEventstopImmediatePropagation只能阻止绑定在 document 上的事件监听器。而合成事件上的e.nativeEvent.stopImmediatePropagation()能阻止合成事件不会冒泡到 document 上。

六、合成事件的事件池

事件池

1、事件池介绍

合成事件对象池,是 React 事件系统提供的一种性能优化方式。合成事件对象在事件池统一管理,不同类型的合成事件具有不同的事件池
事件池未满:React创建新的事件对象,派发给组件;
事件池装满:React从事件池中复用事件对象,派发给组件;
合成事件对象的事件处理函数全部被调用之后,所有属性都会被置为 null

2、React 17版本不使用事件池

e.persist() 将不再生效;因为合成事件不再放入事件池中;

七、常见问题

1、React 事件this指向

JSX回调函数中的this经常会出问题,在class方法不会默认绑定this;

解决办法:

  • 使用 bind 方法绑定 this
    this.clickFun = this.clickFun.bind(this);
class App extends React.Component<any, any> {
  constructor(props: any) {
    super(props);
    this.clickFun = this.clickFun.bind(this);
  }
  clickFun() {
    console.log("React this 指向问题");
  }
  render() {
    return (
        <div onClick={this.clickFun}>React this 指向问题</div>
    );
  }
}
export default App;
  • 将使用this的方法写为使用箭头函数定义
    clickFun = () => {}
class App extends React.Component<any, any> {
  clickFun = () => {
    console.log("React this 指向问题");
  }
  render() {
    return (
        <div onClick={this.clickFun}>React this 指向问题</div>
    );
  }
}
export default App;
  • 在回调函数中使用箭头函数
class App extends React.Component<any, any> {
  // 省略其他代码
  clickFun() {
    console.log("React this 指向问题"); 
  }
  render() {
    return (
        <div onClick={() => this.clickFun()}>React this 指向问题</div>
    );
  }
}
export default App;
2、向事件传递参数问题
const List = [1,2,3,4];
class App extends React.Component<any, any> {
  // 省略其他代码
  clickFun (id) {console.log('当前点击:', id)}
  render() {
    return (
	   <div>
	     <h1>第一种:通过 bind 绑定 this 传参</h1>
	       {
		      	List.map(item => 
		      		<div onClick={this.clickFun.bind(this, item)}>
		      			按钮:{item}
		      		</div>
		      	)
	       }
	     <h1>第二种:通过箭头函数绑定 this 传参</h1>
	    	{
	      		List.map(item =>
			      	<div onClick={() => this.clickFun(item)}>
			      	 	按钮:{item}
			      	</div>
			    )
	        }
	    </div>
    );
  }
}
export default App;

八、支持的事件

  • onFocus
    onFocus 事件在元素聚焦时被调用;比如:用户点击文本输入框,就会调用该事件;
  • onBlur
    onBlur事件在失去焦点时被调用;比如:当用户在已聚焦的文本输入框外点击时,就会被调用;
  • 监听焦点的进入与离开
    React官方文档:合成事件
    可以使用currentTargetrelatedTarget来区分聚焦和失去焦点是否来自父元素外部;下面的例子展示来如何监听一个子元素的聚焦、元素本身的聚焦、以及整个子树进入焦点或离开焦点。
<div
  tabIndex={1}
  onFocus={(e) => {
  	// currentTarget 指的是当前节点
    if (e.currentTarget === e.target) {
      console.log('focused self');
    } else {
      console.log('focused child', e.target);
    }
    if (!e.currentTarget.contains(e.relatedTarget)) {
      // Not triggered when swapping focus between children
      console.log('focus entered self');
    }
  }}
>
	<input id="1" />
	<input id="2" />
</div>
  • 表单事件
onChange、onInput、onReset、onSubmit、onInvalid
  • 通用事件
onError		onLoad
  • 鼠标事件
    onMouseEnteronMouseLeave这两个事件从离开的元素向进入的元素传播,不是正常的冒泡,也没有捕获阶段;
onMOuseMove		onMouseOut		onMouseOver		onMouseUp
onMouseEnter	onMouseLeave 
  • 指针事件:不是每个浏览器都支持指针事件、支持的浏览器有:Chrome、Firefox、Edge、Internet Explorer;
    onPointerEnteronPointerLeave 事件从离开的元素向进入的元素传播,不是正常的冒泡,也没有捕获阶段。
  • 选择事件
  • 触摸事件
  • UI事件(onScroll,从React17开始,onScroll事件在React中不再冒泡)
  • 滚轮事件(onWheel)
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐