前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

React 事件与 DOM 事件有何不同|原理篇

文章学习导航
面试总览

30 秒速记

  • 核心判断:性能优化先建立用户指标与资源、主线程、渲染和网络证据,再定位最长关键路径
  • 原理主线:围绕 「回顾原生 DOM 下的事件流」、「React 事件系统是如何工作的」、「React 事件系统工作流拆解」 建立输入、状态变化与输出之间的因果关系
  • 文章范围:对比了 React 事件与原生 DOM 事件的区别,解析了事件流、事件委托、合成事件机制及其在性能优化和跨浏览器兼容性方面的优势,帮助开发者深入理解 React 事件系统的设计原理和实际应用场景。
  • 边界与代价:实验室分数不等于真实用户体验,平均值也会掩盖长尾;优化一项指标可能转移成本
  • 工程落地:用现场数据确定瓶颈、做最小改动、对照验证并持续监控回归

React 事件不是直接使用原生 DOM 事件,而是基于事件委托封装出的合成事件系统。 原生事件按捕获、目标、冒泡传播;React 则统一监听并收集组件树上的回调,再按相同顺序执行,同时抹平浏览器差异。需要原生对象时,可通过 e.nativeEvent 获取。实现边界要看版本:React 16 主要委托到 document,React 17 以后改为根节点,少数不可冒泡事件还需特殊处理。

这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。

版本校准: 本文若分析 ReactDOM.render、旧生命周期或栈调和,应把它视为理解架构演进的历史路径。React 19 已移除 ReactDOM.render,当前客户端入口使用 createRoot;并发渲染也必须区分可中断的渲染阶段与同步提交阶段。迁移前对照 React 19 官方升级指南。

注:本文逻辑提取自 React 16.13.x。随着 React 版本的更迭,事件系统的实现细节难免有调整,但其设计思想总是一脉相承的,你只要把握住核心逻辑即可。

# 回顾原生 DOM 下的事件流

在浏览器中,我们通过事件监听来实现 JS 和 HTML 之间的交互。一个页面往往会被绑定许许多多的事件,而页面接收事件的顺序,就是事件流。

W3C 标准约定了一个事件的传播过程要经过以下 3 个阶段:

  • 事件捕获阶段
  • 目标阶段
  • 事件冒泡阶段

当事件被触发时,首先经历的是一个捕获过程:事件会从最外层的元素开始“穿梭”,逐层“穿梭”到最内层元素,这个过程会持续到事件抵达它目标的元素(也就是真正触发这个事件的元素)为止;此时事件流就切换到了“目标阶段”——事件被目标元素所接收;然后事件会被“回弹”,进入到冒泡阶段——它会沿着来时的路“逆流而上”,一层一层再走回去。

# DOM 事件流下的性能优化思路:事件委托

在原生 DOM 中,事件委托(也叫事件代理)是一种重要的性能优化手段。这里我通过一道面试题,来快速地帮你回忆相关的知识。

请看下面这段代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <ul id="poem">
    <li>床前明月光</li>
    <li>疑是地上霜</li>
    <li>举头望明月</li>
    <li>低头思故乡</li>
    <li>锄禾日当午</li>
    <li>汗滴禾下土</li>
    <li>谁知盘中餐</li>
    <li>粒粒皆辛苦</li>
    <li>背不动了</li>
    <li>我背不动了</li>
  </ul>
</body>
</html>
@前端进阶之旅: 代码已经复制到剪贴板

问:在这段 HTML 渲染出的界面里,我希望做到点击每一个 li 元素,都能输出它内在的文本内容。你会怎么做?

一个比较直观的思路是让每一个 li 元素都去监听一个点击动作,按照这个思路写出来的代码是这样的:

<script>
  // 获取 li 列表
  var liList = document.getElementsByTagName('li')
  // 逐个安装监听函数
  for (var i = 0; i < liList.length; i++) {
    liList[i].addEventListener('click', function (e) {
      console.log(e.target.innerHTML)
    })
  }
</script>
@前端进阶之旅: 代码已经复制到剪贴板

我们当然可以像这样给 10 个 li 安装 10 次监听函数,但这样不仅累,开销也大。10 个监听函数做的还都是一模一样的事情,也不够优雅。怎么办呢?事件冒泡!

对于这 10 个 li 来说,无论点击动作发生在哪个 li 上,点击事件最终都会被冒泡到它们共同的父亲——ul 元素上去,所以我们完全可以让 ul 来帮忙感知这个点击事件。

既然 ul 可以帮忙感知事件,那它能不能帮忙处理事件呢?答案是能,因为我们有e.target。ul 元素可以通过事件对象中的 target 属性,拿到实际触发事件的那个元素,针对这个元素分发事件处理的逻辑,做到真正的“委托”。

按照这个思路,我们就可以丢掉 for 循环来写代码了,以下是用事件代理来实现同样效果的代码:

var ul = document.getElementById('poem')

ul.addEventListener('click', function(e){
  console.log(e.target.innerHTML)
})
@前端进阶之旅: 代码已经复制到剪贴板

这里再强调一下 e.target 这个属性,它指的是触发事件的具体目标,它记录着事件的源头。所以说,不管咱们的监听函数在哪一层执行,只要我拿到这个 e.target,就相当于拿到了真正触发事件的那个元素。拿到这个元素后,我们完全可以模拟出它的行为,实现无差别的监听效果。

像这样利用事件的冒泡特性,把多个子元素的同一类型的监听逻辑,合并到父元素上通过一个监听函数来管理的行为,就是事件委托。通过事件委托,我们可以减少内存开销、简化注册步骤,大大提高开发效率。

这绝妙的事件委托,正是 React合成事件的灵感源泉。

# React 事件系统是如何工作的

React 的事件系统沿袭了事件委托的思想。在 React 中,除了少数特殊的不可冒泡的事件(比如媒体类型的事件)无法被事件系统处理外,绝大部分的事件都不会被绑定在具体的元素上,而是统一被绑定在页面的 document 上。当事件在具体的 DOM 节点上被触发后,最终都会冒泡到 document 上,document 上所绑定的统一事件处理程序会将事件分发到具体的组件实例。

注意:在react17以后,绝大部分的事件都不会被绑定在具体的元素上,而是统一被绑定在页面的 root节点上,而不是document

在分发事件之前,React 首先会对事件进行包装,把原生 DOM 事件包装成合成事件。

# 认识 React 合成事件

合成事件是 React 自定义的事件对象,它符合W3C规范,在底层抹平了不同浏览器的差异,在上层面向开发者暴露统一的、稳定的、与 DOM 原生事件相同的事件接口。开发者们由此便不必再关注烦琐的兼容性问题,可以专注于业务逻辑的开发。

虽然合成事件并不是原生 DOM 事件,但它保存了原生 DOM 事件的引用。当你需要访问原生 DOM 事件对象时,可以通过合成事件对象的 e.nativeEvent 属性获取到它,如下图所示

e.nativeEvent 将会输出 MouseEvent 这个原生事件,如下图所示:

到这里,大家就对 React 事件系统的基本原理,包括合成事件的基本概念有了一定的了解。接下来,我们将在此基础上结合 React 源码和调用栈,对事件系统的工作流进行深入的拆解。

# React 事件系统工作流拆解

既然是事件系统,那就逃不出“事件绑定”和“事件触发”这两个关键动作。首先让我们一起来看看事件的绑定是如何实现的

# 事件的绑定

事件的绑定是在组件的挂载过程中完成的,具体来说,是在 completeWork 中完成的。这里需要你回忆起来的是 completeWork 中的以下三个动作:

completeWork 内部有三个关键动作:创建 DOM 节点(createInstance)、将 DOM 节点插入到 DOM 树中(appendAllChildren)、为 DOM 节点设置属性(finalizeInitialChildren)。

其中“为 DOM 节点 设置属性” 这个环节,会遍历 FiberNode 的 props key。当遍历到事件相关的 props 时,就会触发事件的注册链路。整个过程涉及的函数调用栈如下图所示:

这些函数之间是如何各司其职、打好“配合”的呢?请看下面这张工作流大图:

从图中可以看出,事件的注册过程是由 ensureListeningTo 函数开启的。在 ensureListeningTo 中,会尝试获取当前 DOM 结构中的根节点(这里指的是 document 对象),然后通过调用 legacyListenToEvent,将统一的事件监听函数注册到 document 上面

在 legacyListenToEvent 中,实际上是通过调用 legacyListenToTopLevelEvent 来处理事件和 document 之间的关系的。 legacyListenToTopLevelEvent 直译过来是“监听顶层的事件”,这里的“顶层”就可以理解为事件委托的最上层,也就是 document 节点。在 legacyListenToTopLevelEvent 中,有这样一段逻辑值得我们注意,请看下图:

listenerMap 是在 legacyListenToEvent 里创建/获取的一个数据结构,它将记录当前 document 已经监听了哪些事件。在 legacyListenToTopLevelEvent 逻辑的起点,会首先判断 listenerMap.has(topLevelType) 这个条件是否为 true。

这里插播一个小的前置知识:topLevelType 在 legacyListenToTopLevelEvent 的函数上下文中代表事件的类型,比如说我尝试监听的是一个点击事件,那么 topLevelType 的值就会是 click,如下图所示:

若事件系统识别到 listenerMap.has(topLevelType) 为 true,也就是当前这个事件 document 已经监听过了,那么就会直接跳过对这个事件的处理,否则才会进入具体的事件监听逻辑。如此一来,即便我们在 React 项目中多次调用了对同一个事件的监听,也只会在 document 上触发一次注册。

为什么针对同一个事件,即便可能会存在多个回调,document 也只需要注册一次监听?因为 React最终注册到 document 上的并不是某一个 DOM 节点上对应的具体回调逻辑,而是一个统一的事件分发函数。这里我将断点打在事件监听函数的绑定动作上,请看下图:

在这段逻辑中,element 就是 document 这个 DOM 元素,如下图所示,它在 legacyListenToEvent 阶段被获取后,又被层层的逻辑传递到了这个位置。

← 剖析 Fiber 架构下 Concurrent 模式的实现原理揭秘 Redux 设计思想与工作原理 →

fe
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合