目录

  • 序言
  • DOM事件流
    • 事件捕获阶段、处于目标阶段、事件冒泡阶段
    • addEventListener 方法
  • React 事件概述
  • 事件注册
    • document 上注册
    • 回调函数存储
  • 事件分发
  • 小结
  • 参考

1.序言

React 有一套自己的事件系统,其事件叫做合成事件。为什么 React 要自定义一套事件系统?React 事件是如何注册和触发的?React 事件与原生 DOM 事件有什么区别?带着这些问题,让我们一起来探究 React 事件机制的原理。为了便于理解,此篇分析将尽可能用图解代替贴 React 源代码进行解析。

2.DOM事件流

首先,在正式讲解 React 事件之前,有必要了解一下 DOM 事件流,其包含三个流程:事件捕获阶段、处于目标阶段和事件冒泡阶段。

W3C协会早在1988年就开始了DOM标准的制定,W3C DOM标准可以分为 DOM1、DOM2、DOM3 三个版本。

从 DOM2 开始,DOM 的事件传播分三个阶段进行:事件捕获阶段、处于目标阶段和事件冒泡阶段。

(1)事件捕获阶段、处于目标阶段和事件冒泡阶段

示例代码:

<html>
<body>
<div id="outer">
<p id="inner">Click me!</p>
</div>
</body>
</html>

上述代码,如果点击 <p>元素,那么 DOM 事件流如下图:

(1)事件捕获阶段:事件对象通过目标节点的祖先 Window 传播到目标的父节点。

(2)处于目标阶段:事件对象到达事件目标节点。如果阻止事件冒泡,那么该事件对象将在此阶段完成后停止传播。

(3)事件冒泡阶段:事件对象以相反的顺序从目标节点的父项开始传播,从目标节点的父项开始到 Window 结束。

(2)addEventListener 方法

DOM 的事件流中同时包含了事件捕获阶段和事件冒泡阶段,而作为开发者,我们可以选择事件处理函数在哪一个阶段被调用。

addEventListener() 方法用于为特定元素绑定一个事件处理函数。addEventListener 有三个参数:

element.addEventListener(event, function, useCapture)

另外,如果一个元素(element)针对同一个事件类型(event),多次绑定同一个事件处理函数(function),那么重复的实例会被抛弃。当然如果第三个参数capture值不一致,此时就算重复定义,也不会被抛弃掉。

3.React 事件概述

React 根据W3C 规范来定义自己的事件系统,其事件被称之为合成事件 (SyntheticEvent)。而其自定义事件系统的动机主要包含以下几个方面:

(1)抹平不同浏览器之间的兼容性差异。最主要的动机。

(2)事件"合成",即事件自定义。事件合成既可以处理兼容性问题,也可以用来自定义事件(例如 React 的 onChange 事件)。

(3)提供一个抽象跨平台事件机制。类似 VirtualDOM 抽象了跨平台的渲染方式,合成事件(SyntheticEvent)提供一个抽象的跨平台事件机制。

(4)可以做更多优化。例如利用事件委托机制,几乎所有事件的触发都代理到了 document,而不是 DOM 节点本身,简化了 DOM 事件处理逻辑,减少了内存开销。(React 自身模拟了一套事件冒泡的机制)

(5)可以干预事件的分发。V16引入 Fiber 架构,React 可以通过干预事件的分发以优化用户的交互体验。

注:「几乎」所有事件都代理到了 document,说明有例外,比如audiovideo标签的一些媒体事件(如 onplay、onpause 等),是 document 所不具有,这些事件只能够在这些标签上进行事件进行代理,但依旧用统一的入口分发函数(dispatchEvent)进行绑定。

4.事件注册

React 的事件注册过程主要做了两件事:document 上注册、存储事件回调。

(1)document 上注册

在 React 组件挂载阶段,根据组件内的声明的事件类型(onclick、onchange 等),在 document 上注册事件(使用addEventListener),并指定统一的回调函数 dispatchEvent。换句话说,document 上不管注册的是什么事件,都具有统一的回调函数 dispatchEvent。也正是因为这一事件委托机制,具有同样的回调函数 dispatchEvent,所以对于同一种事件类型,不论在 document 上注册了几次,最终也只会保留一个有效实例,这能减少内存开销。

示例代码:

function TestComponent() {
handleFatherClick=()=>{
// ...
} handleChildClick=()=>{
// ...
} return <div className="father" onClick={this.handleFatherClick}>
<div className="child" onClick={this.handleChildClick}>child </div>
</div>
}

上述代码中,事件类型都是onclick,由于 React 的事件委托机制,会指定统一的回调函数 dispatchEvent,所以最终只会在 document 上保留一个 click 事件,类似document.addEventListener('click', dispatchEvent),从这里也可以看出 React 的事件是在 DOM 事件流的冒泡阶段被触发执行。

(2)存储事件回调

React 为了在触发事件时可以查找到对应的回调去执行,会把组件内的所有事件统一地存放到一个对象中(listenerBank)。而存储方式如上图,首先会根据事件类型分类存储,例如 click 事件相关的统一存储在一个对象中,回调函数的存储采用键值对(key/value)的方式存储在对象中,key 是组件的唯一标识 id,value 对应的就是事件的回调函数。

React 的事件注册的关键步骤如下图:

5.事件分发

事件分发也就是事件触发。React 的事件触发只会发生在 DOM 事件流的冒泡阶段,因为在 document 上注册时就默认是在冒泡阶段被触发执行。

其大致流程如下:

  1. 触发事件,开始 DOM 事件流,先后经过三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段
  2. 当事件冒泡到 document 时,触发统一的事件分发函数 ReactEventListener.dispatchEvent
  3. 根据原生事件对象(nativeEvent)找到当前节点(即事件触发节点)对应的 ReactDOMComponent 对象
  4. 事件的合成
    1. 根据当前事件类型生成对应的合成对象
    2. 封装原生事件对象和冒泡机制
    3. 查找当前元素以及它所有父级
    4. 在 listenerBank 中查找事件回调函数并合成到 events 中
  5. 批量执行合成事件(events)内的回调函数
  6. 如果没有阻止冒泡,会将继续进行 DOM 事件流的冒泡(从 document 到 window),否则结束事件触发

注:上图中阻止冒泡是指调用stopImmediatePropagation 方法阻止冒泡,如果是调用stopPropagation阻止冒泡,document 上如果还注册了同类型其他的事件,也将会被触发执行,但会正常阻断 window 上事件触发。了解两者之间的详细区别

示例代码:

class TestComponent extends React.Component {

  componentDidMount() {
this.parent.addEventListener('click', (e) => {
console.log('dom parent');
})
this.child.addEventListener('click', (e) => {
console.log('dom child');
})
document.addEventListener('click', (e) => {
console.log('document');
})
document.body.addEventListener('click', (e) => {
console.log('body');
})
window.addEventListener('click', (e) => {
console.log('window');
})
} childClick = (e) => {
console.log('react child');
} parentClick = (e) => {
console.log('react parent');
} render() {
return (
<div class='parent' onClick={this.parentClick} ref={ref => this.parent = ref}>
<div class='child' onClick={this.childClick} ref={ref => this.child = ref}>
Click me!
</div>
</div>)
}
}

点击 child div 时,其输出如下:

在 DOM 事件流的冒泡阶段先后经历的元素:child <div> -> parent <div> -> <body> -> <html> -> document -> window,因此上面的输出符合预期。

6.小结

React 合成事件和原生 DOM 事件的主要区别:

(1)React 组件上声明的事件没有绑定在 React 组件对应的原生 DOM 节点上。

(2)React 利用事件委托机制,将几乎所有事件的触发代理(delegate)在 document 节点上,事件对象(event)是合成对象(SyntheticEvent),不是原生事件对象,但通过 nativeEvent 属性访问原生事件对象。

(3)由于 React 的事件委托机制,React 组件对应的原生 DOM 节点上的事件触发时机总是在 React 组件上的事件之前。

7.参考

javascript中DOM0,DOM2,DOM3级事件模型解析

Event dispatch and DOM event flow

EventTarget.addEventListener() - Web API 接口参考| MDN

合成事件

谈谈React事件机制和未来(react-events)

React源码解读系列 – 事件机制

一文吃透 react 事件机制原理

深入理解React:事件机制原理的更多相关文章

  1. 【React】354- 一文吃透 React 事件机制原理

    大纲 主要分为4大块儿,主要是结合源码对 react事件机制的原理 进行分析,希望可以让你对 react事件机制有更清晰的认识和理解. 当然肯定会存在一些表述不清或者理解不够标准的地方,还请各位大神. ...

  2. 深入理解Spring事件机制(一):广播器与监听器的初始化

    前言 Spring 从 3.x 开始支持事件机制.在 Spring 的事件机制中,我们可以令一个事件类继承 ApplicationEvent 类,然后将实现了 ApplicationListener ...

  3. 剖析Qt的事件机制原理

    版权声明 请尊重原创作品.转载请保持文章完整性,并以超链接形式注明原始作者“tingsking18”和主站点地址,方便其他朋友提问和指正. QT源码解析(一) QT创建窗口程序.消息循环和WinMai ...

  4. 详解 QT 源码之 Qt 事件机制原理

    QT 源码之 Qt 事件机制原理是本文要介绍的内容,在用Qt写Gui程序的时候,在main函数里面最后依据都是app.exec();很多书上对这句的解释是,使 Qt 程序进入消息循环.下面我们就到ex ...

  5. 深入理解 Android 消息机制原理

    欢迎大家前往腾讯云社区,获取更多腾讯海量技术实践干货哦~ 作者:汪毅雄 导语: 本文讲述的是Android的消息机制原理,从Java到Native代码进行了梳理,并结合其中使用到的Epoll模型予以介 ...

  6. 源码看React 事件机制

    对React熟悉的同学都知道,React中的事件机制并不是原生的那一套,事件没有绑定在原生DOM上,发出的事件也是对原生事件的包装.那么这一切是怎么实现的呢? 事件注册 首先还是看我们熟悉的代码 &l ...

  7. react事件机制

    1. react的事件是合成事件((Synethic event),不是原生事件 <button onClick={this.handleClick}></button> &l ...

  8. 深入理解DOM事件机制系列第四篇——事件模拟

    × 目录 [1]引入 [2]模拟机制 [3]自定义事件 前面的话 事件是网页中某个特别的瞬间,经常由用户操作或通过其他浏览器功能来触发.但实际上,也可以使用javascript在任意时刻来触发特定的事 ...

  9. 剖析Qt的事件机制原理(源代码级别)

    在用Qt写Gui程序的时候,在main函数里面最后依据都是app.exec();很多书上对这句的解释是,使Qt程序进入消息循环.下面我们就到exec()函数内部,来看一下他的实现原理.Let's go ...

随机推荐

  1. 分布式项目开发-springmvc.xmll基础配置

    基础步骤: 1 包扫描 2 驱动开发 3 视图解析器 4 文件上传解析器 5 拦截器 6 静态资源 <beans xmlns="http://www.springframework.o ...

  2. ​云中奈飞(一):Netflix的上云之旅

    作者按: Netflix(译为奈飞/网飞)公司自1997年创立以来,已发展成为美国最大的互联网流媒体服务商.它从2008到2015年间长达七年的将其所有IT系统从自有数据中心迁移到AWS之上的旅程,在 ...

  3. 爱奇艺|B站|优酷|腾讯视频高清无水印视频下载方法(软件工具教程)

    导读:经常在大型视频网站平台上看到一些很价值和视频,希望能高清无水印下载到本地学习观看,今天小程序定制开发代码哥DaiMaGe6给大家分享一招免费下载全网高清无水印视频的方法. 高清无水印视频下载工具 ...

  4. Rocket - diplomacy - misaligned

    https://mp.weixin.qq.com/s/poCJBcx45clXHm6Uuv8M6w 介绍AddressSet.misaligned的实现.之前介绍的比较概括,也有偏差.这里根据实际执行 ...

  5. Java实现蓝桥杯VIP算法训练 奇变的字符串

    试题 算法训练 奇变的字符串 资源限制 时间限制:1.0s 内存限制:256.0MB 问题描述 将一个字符串的奇数位(首位为第0位)取出,将其顺序弄反,再放回原字符串的原位置上. 如字符串" ...

  6. Java实现 蓝桥杯VIP 算法提高 最长公共子序列

    算法提高 最长公共子序列 时间限制:1.0s 内存限制:256.0MB 问题描述 给定两个字符串,寻找这两个字串之间的最长公共子序列. 输入格式 输入两行,分别包含一个字符串,仅含有小写字母. 输出格 ...

  7. SQL Server实现 LeetCode 176 第二高的薪水

    176. 第二高的薪水 SQL架构 编写一个 SQL 查询,获取 Employee 表中第二高的薪水(Salary) . +----+--------+ | Id | Salary | +----+- ...

  8. Java实现蓝桥杯正则问题

    题目描述 考虑一种简单的正则表达式: 只由 x ( ) | 组成的正则表达式. 小明想求出这个正则表达式能接受的最长字符串的长度. 例如 ((xx|xxx)x|(x|xx))xx 能接受的最长字符串是 ...

  9. java实现第六届蓝桥杯三角形面积

    三角形面积 题目描述 如图1所示.图中的所有小方格面积都是1. 那么,图中的三角形面积应该是多少呢? 请填写三角形的面积.不要填写任何多余内容或说明性文字. 28 简单的数学平面几何问题: 大正方形面 ...

  10. Linux 日志轮替

    日志轮替包括两个方面的内容:切割日志文件,轮换日志文件 日志文件的命令规则 如果配置文件中有dateext参数,那么日志文件的后缀会是日期,例如:yum.log-20200424,这样,文件名不会重叠 ...