原理 - 事件原理 - 图1

问题

问:React 为什么有自己的事件系统?
简答:

  1. 统一管理事件;
  2. 跨浏览器、跨平台兼容性好。

答:不同浏览器,对事件处理存在兼容性问题。若要实现兼容全浏览器事件系统,需抹平各个浏览器事件系统的差异。
v17 前,事件绑定在document,之后绑定在应用对应容器container上,将事件绑定在同一容器统一管理。
事件不是绑定在真实 DOM 上,所以 React 需模拟一套事件流:事件捕获 —> 事件源 —> 事件冒泡,同时重新事件源event
React 事件系统可分为三个阶段:

  1. 事件合成系统,初始化会注册不同的事件插件;
  2. 在一次渲染中,对事件标签上的事件进行收集,在container上注册事件;
  3. 一次用户交互,事件触发,到事件执行一系列过程。

问:如何处理事件源对象?


问:什么是事件合成?
答:
事件合成概念:React 应用中,元素绑定的事件并不是原生事件,而是 React 的合成事件。如:onClick是由click合成,onChangeblur\focus\change\keydown\keyup合成。

  1. React事件不是绑定在元素上,而是绑定在顶部容器上,v17之前绑定在document上,v17之后绑定在app容器上,利于一个HTML下存在多个应用(微前端);
  2. 绑定事件不是一次性绑定上所以事件,是发现啥事件,绑定啥事件;

问:如何实现批量更新?


问:为什么不能用**return false**来阻止事件的默认行为?
答:原生事件:e.preventDefault() / return false来阻止事件默认行为。但在 React 中return false无效。
React 事件:在 React 中事件源(e)已被重写,只能通过e.preventDefault()来阻止事件默认行为,此方法并非原生事件。
问:React 如何模拟阻止事件冒泡?
答:(重点:事件队列执行)
for循环,执行事件队列时,若事件源event中有event.stopPropagation()则会跳出循环break,队列后边的事件将不再执行,以此来模拟阻止事件冒泡。


问:事件系统如何模拟冒泡、捕获阶段?
答:如图:第三步:形成事件执行队列。(注意:使用while循环进行收集,直到instance === null

  • 第一步通过原生 DOM 找到对应 fiber 对象,根据此 fiber 向上遍历,遇到 元素数据类型 就收集,收集到数组中;
  • 若遇到onClickCapture捕获事件,就会通过unshift添加到数组前边,模拟捕获阶段;
  • 若遇到onClick冒泡事件,就会通过push添加到数组后边,模拟冒泡阶段;
  • 一直收集到最顶层 App,形成执行队列,接下来就依次执行队列中的函数。

问:如何通过 DOM 元素找到与之对应的 fiber 对象?
答:如上图。


问:事件是绑定在真实 DOM 上吗?若不是,那绑定在哪里?(考察:事件绑定)
答:给元素绑定的事件,会保存在 DOM 元素对应的 fiber 对象的memorizedProps属性上。
绑定在document上的事件,是 React 统一的事件处理函数dispatchEvent,React 通过这个函数去代理事件逻辑,包括 React 批量更新等逻辑。
React 事件触发,首先执行的就是dispatchEvent,那dispatchEvent如何知道是什么事件触发的呢?事件注册时,通过bind已将参数绑定到dispatchEvent上了。如下,绑定click事件:

  1. const listener = dispatchEvent.bind(null, 'click', eventSystemFlags, document)
  2. // 真正实现绑定
  3. document.addEventListener('click', listener, false)

问:v17 事件系统有哪些改变?
问:每此点击事件都需要重新收集每个节点的事件码?
答:是的。因为有些元素节点上绑定的事件是动态的。

知识点

React 事件系统中的“假”。

  1. 给元素绑定的事件,不是真正的事件处理函数;
  2. 在冒泡/捕获阶段绑定的事件,也不是在冒泡/捕获阶段执行的;
  3. 事件处理函数中拿到的事件源event,也不是真正的事件源对象。