一文帮你熟悉 React17 事件机制 (一)
React 有一套自己的事件系统,其事件叫做合成事件。为什么 React 要自定义一套事件系统?React 事件是如何注册和触发的?React 事件与原生 DOM 事件有什么区别?带着这些问题,让我们一起来探究 React 事件机制的原理。
此篇react代码分析使用 build 后的react 源码进行解析, build 后的代码相对聚合,函数变量名称变化不大,方便调试,更便于理解,减少心智负担。
在深入了解 React 事件机制之前,先简单回顾下 js事件机制。
js事件机制
事件流
事件流又称为事件传播, DOM2 级事件规定一般事件触发都会经历三个阶段:
- 捕获阶段: 事件从window开始, 自上而下一直传播到目标元素(event.target 目标触发元素)的阶段
- 目标阶段: 事件真正的触发元素处理事件的阶段 (event.target)
- 冒泡阶段: 从目标元素(event.target) 开始, 自下而上一直传播到window的阶段
- 若阻止事件的传播, 可以在指定节点的事件监听器通过
event.stopPropagation()
- 捕获阶段阻止: 阻止后续捕获 和 冒泡 传播
- 冒泡阶段阻止: 阻止后续冒泡 传播
- 有些事件是没有冒泡阶段的, 如scroll、blur、及各种媒体事件等
事件绑定类型
- DOM0级
- 绑定在事件冒泡阶段, 事件内部决定无法改变
element.onclick = function() {}
- DOM2级: 标准事件模式(addEventListener事件监听器)
- 可以改变事件绑定的阶段的, 第三个参数控制, 绑定 冒泡/捕获 由对应阶段触发
- false(默认): 绑定在事件冒泡阶段
- true: 绑定在捕获阶段
element.addEventListener('click', fn, true);
- 可以改变事件绑定的阶段的, 第三个参数控制, 绑定 冒泡/捕获 由对应阶段触发
绑定元素 & 目标元素
- 绑定元素: 把事件处理函数绑定在此元素上面:
event.currentTarget
- 目标元素: 用户操作界面用户触发的元素:
event.target
<ul id="parent">
<li id="sub">sub</li>
</ul>
<script>
const parent = document.getElementById('parent');
const sub = document.getElementById('sub')
parent.onclick = function handleParent(e) {
console.log('handleParent', {
target: e.target,
currentTarget: e.currentTarget
});
}
sub.onclick = function handleSub(e) {
console.log('handleSub', {
target: e.target,
currentTarget: e.currentTarget
});
}
</script>
// 点击 sub
handleSub {target: li#sub, currentTarget: li#sub}
handleParent {target: li#sub, currentTarget: ul#parent}
事件的执行顺序
- 同一个绑定元素, 遵循先绑定的先执行原则
- 以
onclick
的方式绑定, 对同一个元素重复绑定的话, 后面的会覆盖前面的 - 以
addEventListener
方式绑定, 同一个元素绑定多少次, 就会执行多少次 - 在DOM中直接使用
onclick
, 则onclick
的绑定是早于addEventListener
的
事件委托
事件委托是为了减少绑定元素个数和事件处理函数而产生的, 绑定在父级元素, 利用事件冒泡去触发父级事件处理函数的一种技巧
<ul id="parent">
<li>1</li>
<li>2</li>
<li>3</li>
...
</ul>
<script>
const parent = document.getElementById('parent')
parent.addEventListener('click', (event)=>{
// event.target: 对应点击元素
})
</script>
事件对象event
- event.target:触发事件的那个节点, 即事件最初发生的节点
- event.currentTarget:正在执行的监听函数所绑定的那个节点
- event.path:事件冒泡的顺序
- event.timeStamp:从事件开始到事件被创建所经过的毫秒数
- event.type:事件的类型, 比如click, change
- event.isTrusted:表示事件是否是真实用户触发, true表示是真实用户触发, false表示脚本触发
- event.preventDefault():取消事件的默认行为
- a标签 默认跳转到一个新网址, 如果阻止默认行为, 就不会跳转
- event.stopPropagation():当有event对象时, 阻止事件冒泡
- window.event.cancelBubble = true:当没有event对象时, 阻止事件冒泡。
- 直接访问window.event 是undefined,只有在事件触发过程中有效
- event.stopImmediatePropagation():阻止同一个事件的其他监听函数被调用
- 对同一个元素绑定了两个click事件, 如果在第一个click事件写了event.stopImmediatePropagation(), 那么它的其他点击事件就都不会被触发
正文开始
React 17 事件主要通过委托挂载到 container (容器) 节点上, 但并不是所有的事件都是这样处理的, 无冒泡事件委托在dom 元素上, selectionchange 委托在document 上, portal 事件委托在 createPortal container(容器) 节点上
- 合成事件 (SyntheticEvent): 自定义事件
- React 根据W3C 规范来定义自己的事件系统, 其事件被称之为合成事件 (SyntheticEvent)
- React 中定义 on + Click(原生事件首字母大写), 当然也可以使用 handleClick, handleClickCapture, react 里使用 on
- 写在组件里的事件 onClick, onChange, onClickCapture ....
- React 触发事件 由 不同原生事件 模拟合成的事件
- onFocus 是由 ['focusin'] 事件合成的
- onChange 由 ['change', 'click', 'focusin', 'focusout', 'input', 'keydown', 'keyup', 'selectionchange'] 事件合成
- 举例: 一个 input 框, 在React 中需要绑定 onChange 事件就可以, 如果是原生我们需要绑定 change, input, focusout 等事件才可以, React 将这些事件操作合成 一个 onChange 提供给我们使用
- React 可以拓展给我们 由多个原生事件模拟合成 需要的 合成事件
- 原生事件: 浏览器里存在 并会发生的 UI Events
- click, change ....
- 合成事件对象: React 进行封装过的 Event 对象
- 合成事件系统: React 来处理事件机制的系统
- container 节点: React 应用挂载的 DOM 节点, 而不是 document
- 事件委托: 将事件绑定在父节点上统一处理, 减少事件绑定数量
- 也就是说委托的父节点元素 以及 下级 有且只有父节点绑定了事件, 其他元素不再绑定事件, 统一委托 父节点 事件处理
目的
- 使用事件代理统一接收原生事件的触发,使得真实 DOM 上不用绑定事件 (可能触发无意义事件收集)
- 减少内存消耗 和 动态事件绑定: 整个文档树 有成百上千事件, 那这种事件绑定 占用的内存 和 时间是非常大
- 解决跨平台问题, 类似 VirtualDOM 抽象了跨平台的渲染方式, 合成事件(SyntheticEvent)提供一个抽象的跨平台事件机制
- 合成事件对象, 抹平处理浏览器差异
- 阻止事件传播
- event.stopPropagation() 或 event.cancelBubble = true
- 在React中只写event.stopPropagation()
- 合成事件: 多种事件合成 统一 合成事件触发, 方便使用
- 阻止事件传播
- 开发友好: 事件冒泡 & 捕获 统一写法, 更明确事件触发时机 onClick 或 onClickCapture
- 劫持事件触发可以明确触发了什么事件, 通过什么原生事件调用的真实事件
- 通过对原生事件的优先级定义进而确定真实事件的优先级
- 进而可以确定真实事件内触发的更新是什么优先级
- 最后可以决定对应的更新时机
React 合成事件
合成事件使用
- React 事件的命名采用小驼峰式 (camelCase), 而不是纯小写
- 冒泡 和 捕获 需要定义不同合成事件, click 为例子: 冒泡阶段用 onClick, 捕获阶段用 onClickCapture
- React事件中不通过返回false 阻止默认行为, 必须调用 event.preventDefault()
- React事件执行回调时的上下文不在组件内部, 需要注意this的指向
- React 能够合成一些事件 提供开发者使用
- 多种事件合成一种 抹平兼容处理, 不需要 处理多种事件 兼容操作完整性
- onMouseEnter 依赖 [mouseout, mouseover], 是 React 使用 mouseout 和 mouseover 模拟合成的
抹平浏览器差异
- React通过事件normalize 在不同浏览器中拥有一致的属性
- React声明了各种事件的接口, 以此来抹平浏览器中的差异:
- 接口中的字段值为0, 则直接使用原生事件的值
- 接口中字段的值为函数, 则会以原生事件作为入参, 调用该函数来返回抹平了浏览器差异的值
// 基础事件接口,timeStamp需要抹平差异
/**
* @interface Event
* @see http://www.w3.org/TR/DOM-Level-3-Events/
*/
var EventInterface = {
eventPhase: 0,
bubbles: 0,
cancelable: 0,
timeStamp: function (event) {
return event.timeStamp || Date.now();
},
defaultPrevented: 0,
isTrusted: 0
};
var UIEventInterface = _assign({}, EventInterface, {
view: 0,
detail: 0
});
/**
* @interface MouseEvent
* @see http://www.w3.org/TR/DOM-Level-3-Events/
*/
var MouseEventInterface = _assign({}, UIEventInterface, {
screenX: 0,
screenY: 0,
clientX: 0,
clientY: 0,
pageX: 0,
pageY: 0,
ctrlKey: 0,
shiftKey: 0,
altKey: 0,
metaKey: 0,
getModifierState: getEventModifierState,
button: 0,
buttons: 0,
relatedTarget: function (event) {
if (event.relatedTarget === undefined) return event.fromElement === event.srcElement ? event.toElement : event.fromElement;
return event.relatedTarget;
},
movementX: function (event) {
if ('movementX' in event) {
return event.movementX;
}
updateMouseMovementPolyfillState(event);
return lastMovementX;
},
movementY: function (event) {
if ('movementY' in event) {
return event.movementY;
}
return lastMovementY;
}
});
// ......
不同的类型的事件其字段有所不同, React 以不同事件接口的合成事件构造函数的工厂函数, 通过传入不一样的事件接口返回对应事件的合成事件构造函数, 在事件触发回调时根据触发的事件类型判断使用哪种类型的合成事件构造函数来实例化合成事件
// 辅助函数, 返回 true false
function functionThatReturnsTrue() {
return true;
}
function functionThatReturnsFalse() {
return false;
}
function createSyntheticEvent(Interface) {
// 合成事件构造函数
function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
// ...
/**
* 抹平字段在浏览器间的差异
* 内部要么函数 & 要么是属性 xxx: 0
* {
* xxx: 0,
* // or
* xxx: function () {}
* }
*/
for (var _propName in Interface) {
if (!Interface.hasOwnProperty(_propName)) {
// 该接口没有这个字段, 不拷贝
continue;
}
// 拿到事件接口对应的值
var normalize = Interface[_propName];
// 接口对应字段函数, 进入if分支, 执行函数拿到值
if (normalize) {
// 抹平了浏览器差异后的值
this[_propName] = normalize(nativeEvent);
} else {
// 接口对应值是0, 则直接取原生事件对应字段值
this[_propName] = nativeEvent[_propName];
}
}
// ...
return this;
}
_assign(SyntheticBaseEvent.prototype, {
// 阻止默认事件
preventDefault: function () {
// ...
},
// 阻止捕获和冒泡阶段中当前事件的进一步传播
stopPropagation: function () {
// ...
},
// 合成事件不使用对象池了, 这个事件是空的没有意义
persist: function () {
},
isPersistent: functionThatReturnsTrue
});
return SyntheticBaseEvent;
}
合成事件的意义
- 统一规范提供合成事件对象, 解决了不同浏览器之间的兼容性差异, 更便于跨平台
- 事件几乎全部委托到 挂载 container 节点,而非 DOM 节点本身, 减少了内存消耗
- 对事件进行归类 ( SyntheticMouseEvent、SyntheticUIEvent、SyntheticDragEvent 等), 在事件产生的任务上进行了优先级划分, 从而起到干预事件的作用
注意: 以下代码是 build 后的代码, 并且省略一些不重要的逻辑, 提示, 警告等, 标注 重点
是本文主要讲解内容
React 合成事件系统
React提供了一种 "顶层注册, 事件收集, 统一触发" 的事件机制, React 事件系统的整个流程可以分为三个过程:
- 框架初始化: 事件初始化注册
- 注册事件 原生事件优先级, 合成事件和原生事件依赖关系, React 支持的所有原生事件集合 等变量
- 处理监听 React 支持的所有原生事件: 事件代理
- 创建了 fiberRoot 后, 在开始构造 fiber 树前
- 触发事件: 事件收集 & 处理事件
框架初始化: 事件初始化注册
变量介绍
- 事件优先级 & 不同优先级对应事件数组集合
// 离散事件,cancel、click、mousedown 这类单点触发不持续的事件,优先级最低
const DiscreteEvent = 0;
// 用户阻塞事件,drag、mousemove、wheel 这类持续触发的事件,优先级相对较高
const UserBlockingEvent = 1;
// 连续事件,load、error、waiting 这类大多与媒体相关的事件为主的事件需要及时响应,所以优先级最高
const ContinuousEvent = 2;
// discreteEventPairs: 离散事件
const discreteEventPairsForSimpleEventPlugin = [
'cancel', 'cancel', 'click', 'click', 'close', 'close',
'contextmenu', 'contextMenu', 'copy', 'copy', 'cut', 'cut',
'auxclick', 'auxClick', 'dblclick', 'doubleClick', // Careful!
'dragend', 'dragEnd', 'dragstart', 'dragStart', 'drop', 'drop',
'focusin', 'focus', // Careful!
'focusout', 'blur', // Careful!
'input', 'input', 'invalid', 'invalid', 'keydown', 'keyDown',
'keypress', 'keyPress', 'keyup', 'keyUp', 'mousedown', 'mouseDown',
'mouseup', 'mouseUp', 'paste', 'paste', 'pause', 'pause',
'play', 'play', 'pointercancel', 'pointerCancel', 'pointerdown', 'pointerDown',
'pointerup', 'pointerUp', 'ratechange', 'rateChange', 'reset', 'reset',
'seeked', 'seeked', 'submit', 'submit', 'touchcancel', 'touchCancel',
'touchend', 'touchEnd', 'touchstart', 'touchStart', 'volumechange', 'volumeChange'
];
// 其他离散事件
const otherDiscreteEvents = [
'change', 'selectionchange', 'textInput',
'compositionstart', 'compositionend', 'compositionupdate'
];
// 用户阻塞事件
const userBlockingPairsForSimpleEventPlugin = [
'drag', 'drag', 'dragenter', 'dragEnter', 'dragexit', 'dragExit',
'dragleave', 'dragLeave', 'dragover', 'dragOver', 'mousemove', 'mouseMove',
'mouseout', 'mouseOut', 'mouseover', 'mouseOver', 'pointermove', 'pointerMove',
'pointerout', 'pointerOut', 'pointerover', 'pointerOver', 'scroll', 'scroll',
'toggle', 'toggle', 'touchmove', 'touchMove', 'wheel', 'wheel'
]; // prettier-ignore
// 连续(动画)事件
const continuousPairsForSimpleEventPlugin = [
'abort', 'abort', ANIMATION_END, 'animationEnd', ANIMATION_ITERATION, 'animationIteration',
ANIMATION_START, 'animationStart', 'canplay', 'canPlay', 'canplaythrough', 'canPlayThrough',
'durationchange', 'durationChange', 'emptied', 'emptied', 'encrypted', 'encrypted',
'ended', 'ended', 'error', 'error', 'gotpointercapture', 'gotPointerCapture',
'load', 'load', 'loadeddata', 'loadedData', 'loadedmetadata', 'loadedMetadata',
'loadstart', 'loadStart', 'lostpointercapture', 'lostPointerCapture', 'playing', 'playing',
'progress', 'progress', 'seeking', 'seeking', 'stalled', 'stalled',
'suspend', 'suspend', 'timeupdate', 'timeUpdate', TRANSITION_END, 'transitionEnd',
'waiting', 'waiting'
];
- 为什么 每个 事件定义了两次?
- 第一个是原生事件名, 第二个是对应 React 驼峰处理的事件名
- 为什么 otherDiscreteEvents 这个没有定义React 驼峰事件
- 此处 React事件 需要多个事件 模拟合成, 设置下原生事件优先级即可
- ['change', 'compositionstart', 'compositionend', 'compositionupdate']
- 模拟合成 到 React 事件 自定义onChange, onSelect, onBeforeInput 事件里面
- ['selectionchange', 'textInput']
- 此处 React事件 需要多个事件 模拟合成, 设置下原生事件优先级即可
ANIMATION_END
ANIMATION_ITERATION
ANIMATION_START
TRANSITION_END
是什么?- 就是对应 不同浏览器 有
Webkit | Moz
前缀的事件名称 - 现代浏览器下就是:
animationend
animationiteration
animationstart
transitionend
- 就是对应 不同浏览器 有
/**
* 使用定义的样式属性 和 事件名称 生成标准供应商前缀(vendor prefixed: 浏览器)前缀的映射
* @param {string} styleProp
* @param {string} eventName
* @returns {object}
*/
function makePrefixMap(styleProp, eventName) {
var prefixes = {};
prefixes[styleProp.toLowerCase()] = eventName.toLowerCase();
prefixes['Webkit' + styleProp] = 'webkit' + eventName;
prefixes['Moz' + styleProp] = 'moz' + eventName;
return prefixes;
}
var vendorPrefixes = {
animationend: makePrefixMap('Animation', 'AnimationEnd'),
animationiteration: makePrefixMap('Animation', 'AnimationIteration'),
animationstart: makePrefixMap('Animation', 'AnimationStart'),
transitionend: makePrefixMap('Transition', 'TransitionEnd')
};
// 已检测到并添加前缀的事件名称
var prefixedEventNames = {};
/**
* 确定正确的 供应商前缀(vendor prefixed: 浏览器) 事件名称
* @param {string} eventName
* @returns {string}
*/
function getVendorPrefixedEventName(eventName) {
if (prefixedEventNames[eventName]) {
return prefixedEventNames[eventName];
} else if (!vendorPrefixes[eventName]) {
return eventName;
}
var prefixMap = vendorPrefixes[eventName];
for (var styleProp in prefixMap) {
if (prefixMap.hasOwnProperty(styleProp) && styleProp in style) {
return prefixedEventNames[eventName] = prefixMap[styleProp];
}
}
return eventName;
}
var ANIMATION_END = getVendorPrefixedEventName('animationend');
var ANIMATION_ITERATION = getVendorPrefixedEventName('animationiteration');
var ANIMATION_START = getVendorPrefixedEventName('animationstart');
var TRANSITION_END = getVendorPrefixedEventName('transitionend');
- eventPriorities: 原生事件及其优先级的映射 (Map)
Map: {
"cancel": 0,
"drag": 1,
"abort": 2,
...
}
- registrationNameDependencies: 合成事件和其依赖的原生事件集合的映射 (这个是一个对象)
- registrationNameDependencies = {};
{
"onCancel": ["cancel"],
"onCancelCapture": ["cancel"],
"onClick": ["click"],
"onClickCapture": ["click"],
// ...
"onMouseLeave": ["mouseout", "mouseover"],
"onMouseEnter": ["mouseout", "mouseover"],
// ....
"onChange": ["change", "click", "focusin","focusout", "input", "keydown", "keyup", "selectionchange"],
"onCancelCapture": ["change", "click", "focusin","focusout", "input", "keydown", "keyup", "selectionchange"],
"onSelect": ["focusout", "contextmenu", "dragend", "focusin", "keydown", "keyup", "mousedown", "mouseup", "selectionchange"],
"onSelectCapture": ["focusout", "contextmenu", "dragend", "focusin", "keydown", "keyup", "mousedown", "mouseup", "selectionchange"],
// ...
}
-
- 对于onClick 和 onClickCapture 事件, 只依赖原生click 事件
- 对于onChange 和 onCancelCapture 事件, 依赖了 change, click, focusin等, 这个事件是React 使用 change, click, focusin等原生事件模拟合成的
- React 能够合成一些哪怕浏览器不支持的事件供使用者代码使用
- topLevelEventsToReactNames: 顶级原生事件 对应 合成事件映射 (Map)
Map: {
"cancel" => "onCancel",
"click" => "onClick",
......
}
- allNativeEvents: React 支持的所有原生事件名称集合 Set
- Set 防止重复
Set: {click, change, .......}
- nonDelegatedEvents: 无冒泡的事件 Set
// 媒体事件名称集合
const mediaEventTypes = [
'abort', 'canplay', 'canplaythrough', 'durationchange', 'emptied', 'encrypted',
'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart', 'pause',
'play', 'playing', 'progress', 'ratechange', 'seeked', 'seeking',
'stalled', 'suspend', 'timeupdate', 'volumechange', 'waiting'
];
// 不需要在冒泡阶段进行事件代理(委托)的原生事件名称集合
const nonDelegatedEvents = new Set(
[
'cancel', 'close', 'invalid', 'load', 'scroll', 'toggle'
].concat(mediaEventTypes)
);
react 使用 事件委托和事件全局变量 为什么如此设计?
为什么设置这些全局变量, 全局变量和事件委托有什么关联? 借用以下例子解释下: 现在有一个小区正在开发, 需要针对小区建立一套自己的预警监听系统
-
政府 对于预警设定了一套统一的 预警类型, 触发警报: 浏览器原生事件
-
一个小区: html
-
正在建设一幢楼: div#container 容器所有内容
-
现在需要系统监听 不同种类的预警信息
- 火灾, 线路问题, 水管问题 等: click, change, select 等不同事件
addEventListener('火灾', handlerFireWarn) addEventListener('线路', handlerLineWarn) addEventListener('水管', handlerWaterWarn) ... // handlerFireWarn // handlerLineWarn // handlerWaterWarn // ... // 针对不同预警 做出对应的处理逻辑
-
现在我们有两种设计:
- 第一种: 每户建立预警监听系统
- 针对成千上万户而言, 我们需要建立 n 个预警监听系统
- 每增加楼m户数, 需要建立m 个 预警监听系统, 这样资源和时间消耗是巨大的
- 由于每户系统相对独立, 预警之后 小区的安保中心 并不清楚哪里出现问题, 如此需要建立每户系统和楼幢系统之间联系, 才能方便快速定位, 如此需要消耗大量资源 和 时间去处理
- 如果在其他社区(浏览器)也要进行预警, 可能受社区 规定, 地理, 气候, 文化等影响, 不同预警需要采取不同的处理措施, 那么我们需要对所有 家庭单独去处理, 这个工作是巨大的
- 第二种: 对 楼幢 建立预警系统, 预警后只需要对预警信息处理分析
- 仅仅需要 针对 楼幢 建立预警系统, 警报预警后, 仅仅需要针对预警信息处理分析即可, 这样对资源 和 事件消耗比较小
- 抽离一套可复用预警机制(定位, 影响等)方法
addEventListener('火灾', handlerFireWarn) addEventListener('线路', handlerLineWarn) handlerFireWarn 和 handlerLineWarn 处理其对应的警报, 并且像定位信息的方式都是可公用的, 仅仅需要对预警操作做处理
- 增加楼幢, 我们可以将之前预警 系统快速应用于新楼, 节约资源和时间的投入
- 仅仅需要 针对 楼幢 建立预警系统, 警报预警后, 仅仅需要针对预警信息处理分析即可, 这样对资源 和 事件消耗比较小
- React 事件系统也是采用第二种方式做处理, 不需要对每个元素所有事件进行监听, 只需要委托 container / document / portal 容器节点 处理不同的事件即可
- 第一种: 每户建立预警监听系统
-
显然, 第二种方式处理会相对 资源, 时间, 触发收集 等方面来说更有优势, 如此使用第二种方式
-
方式确定了, 然后调研发现一些问题
- 如果在其他社区也要进行预警, 可能受社区 规定, 地理, 气候, 文化等影响, 不同预警需要采取不同的处理措施, 需要在 预警信息做一些处理兼容: React 合成事件对象
- 政府设定的预警体系过于繁琐, 不方便处理, 多种相近的预警 完全 可以作为一种 预警处理: React 合成事件
- 政府设定的预警体系不方便自己扩展, 需要提供自定义预警事件 来扩展自己的预警机制: React 合成事件
-
我们定义自己的预警系统, 需要存储 自定义预警事件 和 政府设定的预警体系事件 的关联依赖关系, 方便记录预警并且上报政府
-
现在我们有了一套预警监听机制, 像预警种类(事件), 预警类型优先级 等是固定的, 每应用一套都需要这些预警, 那么我们难道每一次的在新的楼幢使用时候重新建立吗, 显然不用, 如此我们就需要存储它们, 方便使用, 因此就需要开始注册记录下, 后续直接使用, 那么全局变量以及关联关系 就需要被记录存储
React 事件系统也是采用第二种方式做处理, 不需要对每个元素所有事件进行监听, 只需要委托 root / document / portal 根节点 处理不同的事件即可。
- 每户警报处理事件, 警报类型 使用自定义警报处理函数, 因此需要 建立 全局警报 和 自定义警报之间的关联映射关系
- 不同警报优先级不一样, 比如火灾警报需要更早发现并处理, 维护警报优先级映射
框架初始化: 事件初始化注册流程
- 此过程执行在引入的 React 框架时执行, 注册合成事件、原生事件以及事件优先级之间的映射关系, 注册位置是上面介绍的全局变量。
// 这里是 react 源码未构建顺序, 方便理解, 贴了出来, 不然下面 registerEvents$2 $1 $3 有些懵
SimpleEventPlugin.registerEvents(); // ===> registerSimpleEvents()
EnterLeaveEventPlugin.registerEvents(); // ===> registerEvents$2()
ChangeEventPlugin.registerEvents(); // ===> registerEvents$1()
SelectEventPlugin.registerEvents(); // ===> registerEvents$3()
BeforeInputEventPlugin.registerEvents(); // ===> registerEvents()
// 下面是构建后的, 开始提到, 我们看源码是以 build 构建后为主, 结合未构建梳理
registerSimpleEvents(); // 注册大部分事件, 顶级事件
registerEvents$2(); // 注册类似onMouseEnter, onMouseLeave单阶段事件, 只注册冒泡阶段事件
registerEvents$1(); // 注册onChange相关事件, 注册冒泡和捕获阶段两个事件
registerEvents$3(); // 注册onSelect相关事件, 注册冒泡和捕获阶段两个事件
registerEvents(); // 注册onBeforeInput, onCompositionUpdate等相关事件, 注册冒泡和捕获阶段两个事件
registerSimpleEvents
- 即: SimpleEventPlugin.registerEvents()
- 注册大部分事件, 顶级事件
// 注册简单事件, 顶级事件
function registerSimpleEvents() {
registerSimplePluginEventsAndSetTheirPriorities(
discreteEventPairsForSimpleEventPlugin,
DiscreteEvent
);
registerSimplePluginEventsAndSetTheirPriorities(
userBlockingPairsForSimpleEventPlugin,
UserBlockingEvent
);
registerSimplePluginEventsAndSetTheirPriorities(
continuousPairsForSimpleEventPlugin,
ContinuousEvent
);
setEventPriorities(
otherDiscreteEvents,
DiscreteEvent
);
}
function registerSimplePluginEventsAndSetTheirPriorities(eventTypes, priority) {
for (var i = 0; i < eventTypes.length; i += 2) {
// 顶级事件(原生事件): click, dragstart, ...
var topEvent = eventTypes[i];
// 事件名: 对应原生事件 驼峰命名: click, dragStart, ...
var event = eventTypes[i + 1];
// 事件大写 首字母: Click, DragStart, ...
var capitalizedEvent = event[0].toUpperCase() + event.slice(1);
// react 事件: onClick, onDragStart, ...
var reactName = 'on' + capitalizedEvent;
// 收集 原生事件优先级 映射
eventPriorities.set(topEvent, priority);
// 收集 原生事件对应react事件 映射
topLevelEventsToReactNames.set(topEvent, reactName);
// 注册 冒泡 & 捕获 阶段react事件对应原生事件集合 映射
registerTwoPhaseEvent(reactName, [topEvent]);
}
}
// 设置原生事件优先级 集合
function setEventPriorities(eventTypes, priority) {
for (var i = 0; i < eventTypes.length; i++) {
eventPriorities.set(eventTypes[i], priority);
}
}
registerEvents$2
- 即: EnterLeaveEventPlugin.registerEvents()
- 注册类似onMouseEnter, onMouseLeave单阶段事件, 只注册冒泡阶段事件
function registerEvents$2() {
registerDirectEvent('onMouseEnter', ['mouseout', 'mouseover']);
registerDirectEvent('onMouseLeave', ['mouseout', 'mouseover']);
registerDirectEvent('onPointerEnter', ['pointerout', 'pointerover']);
registerDirectEvent('onPointerLeave', ['pointerout', 'pointerover']);
}
registerEvents$1()
- 即: ChangeEventPlugin.registerEvents()
- 注册onChange相关事件, 注册冒泡和捕获阶段两个事件
function registerEvents$1() {
registerTwoPhaseEvent('onChange', ['change', 'click', 'focusin', 'focusout', 'input', 'keydown', 'keyup', 'selectionchange']);
}
registerEvents$3()
- 即: SelectEventPlugin.registerEvents()
- 注册onSelect相关事件, 注册冒泡和捕获阶段两个事件
function registerEvents$3() {
registerTwoPhaseEvent('onSelect', ['focusout', 'contextmenu', 'dragend', 'focusin', 'keydown', 'keyup', 'mousedown', 'mouseup', 'selectionchange']);
}
registerEvents()
- 即: BeforeInputEventPlugin.registerEvents()
- 注册onBeforeInput, onCompositionUpdate等相关事件, 注册冒泡和捕获阶段两个事件
function registerEvents() {
registerTwoPhaseEvent('onBeforeInput', ['compositionend', 'keypress', 'textInput', 'paste']);
registerTwoPhaseEvent('onCompositionEnd', ['compositionend', 'focusout', 'keydown', 'keypress', 'keyup', 'mousedown']);
registerTwoPhaseEvent('onCompositionStart', ['compositionstart', 'focusout', 'keydown', 'keypress', 'keyup', 'mousedown']);
registerTwoPhaseEvent('onCompositionUpdate', ['compositionupdate', 'focusout', 'keydown', 'keypress', 'keyup', 'mousedown']);
}
/**
* 注册 冒泡 & 捕获 阶段react事件对应原生事件集合 映射
* @param {*} registrationName : 'on' + Change .....
* @param {*} dependencies [change, click, ...]
*/
function registerTwoPhaseEvent(registrationName, dependencies) {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + 'Capture', dependencies);
}
/**
* 1. 合成事件 映射 原生事件依赖集合
* 2. 原生事件加入 allNativeEvents 集合: Set 防止重复添加
*/
function registerDirectEvent(registrationName, dependencies) {
// 收集React事件对应原生事件集合 映射
registrationNameDependencies[registrationName] = dependencies;
for (var i = 0; i < dependencies.length; i++) {
// 原生事件收集
allNativeEvents.add(dependencies[i]);
}
}
到这里, 框架初始化事件初始化流程完成, 总结下这里做了什么?
- 事件初始化: 注册React事件 全局变量
- 建立事件映射 并 收集: 存储 原生事件 和 合成事件之间对应关系, 事件优先级对应关系
- 原生事件 => 合成事件 映射: topLevelEventsToReactNames
- 原生事件 => 优先级 映射: eventPriorities
- 合成事件 => 原生事件集合映射: 包含 冒泡 & 捕获 合成事件 registrationNameDependencies
- 收集React支持所有原生事件集合: allNativeEvents
- 第二阶段遍历, 对所有原生事件 进行 冒泡 和 捕获 事件代理委托
委托监听 React 支持的所有原生事件: 事件代理
React 事件委托元素
- container 根节点 (本文主要介绍委托容器)
- 除selectionchange 的所有事件
- 不能冒泡事件 不会代理冒泡阶段
- document
- selectionchange 事件
- DOM 自身元素: 不能冒泡的事件
- nonDelegatedEvents
- React Portals: completeWork portal根节点, 调用 listenToAllSupportedEvents 委托 portal根节点
ReactDOM.createPortal(
<Comp />,
document.getElementById('portal')
)
react16 监听节点事document, react17 委托监听挂载容器节点 container, 有什么优点?
- 收束合成事件系统的影响范围
- 假如有一个微前端应用, 里面有多个React 子应用, React16 导致多个React子应用事件监听会互相影响导致一方错误
- React17将合成事件系统收束到当前 React 应用的 容器container 节点, 这种副作用就消失了
为了方便理解, 下面会将 container 容器节点使用 root 根节点, portal 模式容器使用 portal 根节点
代理事件绑定: root 代理委托
创建了 fiberRoot 后, 在开始构造 fiber 树前, 调用 listenToAllSupportedEvents 处理
function createRootImpl(container, tag, options) {
// ...
{
// React 应用的根 DOM 节点
var rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container;
listenToAllSupportedEvents(rootContainerElement);
}
// ...
}
listenToAllSupportedEvents
/**
* 完成了事件的注册, root上完成所有可代理事件委托
* 1. 防止重复监听
* 2. 遍历allNativeEvents, 调用listenToNativeEvent监听冒泡和捕获阶段的事件
*/
var listeningMarker = '_reactListening' + Math.random().toString(36).slice(2);
function listenToAllSupportedEvents(rootContainerElement) {
// 标识节点是否已经以 react 的方式在所有原生事件上添加监听事件
if (rootContainerElement[listeningMarker]) {
return;
}
rootContainerElement[listeningMarker] = true;
/**
* 遍历所有原生事件
* - 不需要冒泡原生事件, 仅在捕获阶段添加事件代理
* - 其余的事件都需要在捕获、冒泡阶段添加代理事件
*/
allNativeEvents.forEach(function (domEventName) {
if (!nonDelegatedEvents.has(domEventName)) {
// 冒泡
listenToNativeEvent(domEventName, false, rootContainerElement, null);
}
// 捕获
listenToNativeEvent(domEventName, true, rootContainerElement, null);
});
}
}
- root容器节点 上 是否处理过了所有事件的委托, 处理过了直接结束
- 对 allNativeEvents (React 支持所有事件) 原生事件 委托事件监听 (冒泡 + 捕获), 对于没有冒泡的事件 只添加 捕获监听
listenToNativeEvent
- eventSystemFlags: 事件系统标识状态变化不影响理解, 不做详细解释, 可忽略
/**
* - 监听冒泡和捕获阶段的事件, 内部利用了Set, 保证了同样的事件只会被注册一次
* - 调用addTrappedEventListener注册了事件监听
* @param {*} domEventName 事件名称(原生)
* @param {*} isCapturePhaseListener true / false(冒泡)
* @param {*} rootContainerElement root
* @param {*} targetElement null
* @returns
*/
function listenToNativeEvent(domEventName, isCapturePhaseListener, rootContainerElement, targetElement) {
// 默认是 0
var eventSystemFlags = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0;
var target = rootContainerElement;
// 这里可以不看, selectionchange 在 document 上触发, 这里设置 document 代理
if (domEventName === 'selectionchange' && rootContainerElement.nodeType !== DOCUMENT_NODE) {
target = rootContainerElement.ownerDocument; // document
}
if (domEventName === 'selectionchange' && rootContainerElement.nodeType !== DOCUMENT_NODE) {
target = rootContainerElement.ownerDocument; // document
}
// ....
// target 节点上存了一个 Set 类型的值, 内部存储着已经添加监听器的原生事件名称, 目的是为了防止重复添加监听器
// 给dom设置一个属性值(new Set())
// target[`__reactEvents$${Math.random().toString(36).slice(2)}`] = Set<xxx__capture | xxx__bubble>
var listenerSet = getEventListenerSet(target);
/**
* 获取将要放到 listenerSet 里的事件名称
* 'cancel' -> 'cancel__capture' | 'cancel__bubble'
*/
var listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener);
if (!listenerSet.has(listenerSetKey)) {
// 在现阶段 eventSystemFlags 入参常为 0
// 只要是在捕获阶段添加监听器的添加过程中,eventSystemFlags = IS_CAPTURE_PHASE = 1 << 2
if (isCapturePhaseListener) {
// IS_NON_DELEGATED = 1 << 1;
// IS_CAPTURE_PHASE = 1 << 2;
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener);
listenerSet.add(listenerSetKey);
}
- 对于 selectionchange 事件, 设置委托容器为 document
- 委托目标元素上面存在一个
__reactEvents$ + Math.random().toString(36).slice(2)
Set 数据
// internalEventHandlersKey: __reactEvents$ + Math.random().toString(36).slice(2)
function getEventListenerSet(node) {
var elementListenerSet = node[internalEventHandlersKey];
if (elementListenerSet === undefined) {
elementListenerSet = node[internalEventHandlersKey] = new Set();
}
return elementListenerSet;
}
- 判断事件是否已经监听, 已经处理过的事件, 将不再重复添加事件监听
addTrappedEventListener: 目标元素绑定监听事件 (冒泡 & 捕获)
- 大多数事件 绑定元素 root (重点)
- selectionchange 绑定 document
- 不能冒泡事件 自身元素
/**
* - 调用createEventListenerWrapperWithPriority根据事件的优先级创建监听器
* - 之后会根据是冒泡或捕获, 调用对应的事件注册函数
* 事件注册监听就结束了, 在root dom portal document 元素中委托
* @param {*} targetContainer root or document or 元素 or portal
- 大多数 root
- selectionChange document
- 不能冒泡事件 挂载元素
- portal 格式: portal
* @param {*} domEventName 事件名称(原生)
* @param {*} eventSystemFlags 标识
* @param {*} isCapturePhaseListener true/false
* @param {*} isDeferredListenerForLegacyFBSupport (未发现调用)
*/
function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
// 创建带有优先级的事件监听器: 注意非真实的事件, 仅仅是事件派发器
var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags);
// 这段可以忽略, 主要针对移动端 优化滚动性能
var isPassiveListener = undefined;
// 被动事件支持 passiveBrowserEventsSupported (针对移动端滚动优化)
// passive用于浏览器优化页面滚动性能, 在注册事件时多一个passive: true属性
if (passiveBrowserEventsSupported) {
if (domEventName === 'touchstart' || domEventName === 'touchmove' || domEventName === 'wheel') {
isPassiveListener = true;
}
}
targetContainer = targetContainer;
var unsubscribeListener;
// 注册挂载事件监听器: 在原生事件上添加不同阶段的事件监听器 (冒泡 | 捕获)
// element.addEventListener(name, handle, bool)
if (isCapturePhaseListener) {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(targetContainer, domEventName, listener, isPassiveListener);
} else {
unsubscribeListener = addEventCaptureListener(targetContainer, domEventName, listener);
}
} else {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(targetContainer, domEventName, listener, isPassiveListener);
} else {
unsubscribeListener = addEventBubbleListener(targetContainer, domEventName, listener);
}
}
}
function addEventBubbleListener(target, eventType, listener) {
target.addEventListener(eventType, listener, false);
return listener;
}
function addEventCaptureListener(target, eventType, listener) {
target.addEventListener(eventType, listener, true);
return listener;
}
function addEventCaptureListenerWithPassiveFlag(target, eventType, listener, passive) {
target.addEventListener(eventType, listener, {
capture: true,
passive: passive
});
return listener;
}
function addEventBubbleListenerWithPassiveFlag(target, eventType, listener, passive) {
target.addEventListener(eventType, listener, {
passive: passive
});
return listener;
}
- createEventListenerWrapperWithPriority 创建带有优先级的事件监听器
- 对移动端 touchstart, touchmove, wheel 添加 {passive: true} 性能优化
- 分别 对事件 冒泡 和 捕获 注册事件监听器
createEventListenerWrapperWithPriority: 创建带有优先级的事件监听器
- 是事件派发器
- 根据事件优先级, 派发不同的事件监听器
/**
* - 会根据DOM事件名拿到事件的优先级的高低 (离散,用户阻塞,连续), 来调用不同的listenerWrapper
* DiscreteEvent: click, keydown, input => dispatchDiscreteEvent
* UserBlockingEvent: drag, scroll => dispatchUserBlockingUpdate
* ContinuousEvent: play, load => dispatchEvent
* - 区别是执行优先级
* @param {*} targetContainer
* @param {*} domEventName
* @param {*} eventSystemFlags
* @returns
*/
function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
// 从 eventPriorities 中获取当前原生事件的优先级
var eventPriority = getEventPriorityForPluginSystem(domEventName);
var listenerWrapper;
switch (eventPriority) {
// 0: 离散事件, 如click优先级低
case DiscreteEvent:
listenerWrapper = dispatchDiscreteEvent;
break;
// 1: 用户阻塞事件, 如scroll优先级中
case UserBlockingEvent:
listenerWrapper = dispatchUserBlockingUpdate;
break;
// 2: 连续事件, 如load优先级高
case ContinuousEvent:
default:
listenerWrapper = dispatchEvent;
break;
}
// 无论是 dispatchDiscreteEvent | dispatchUserBlockingUpdate | dispatchEvent
// 原生事件执行时候触发: 最终执行的是 dispatchEvent 事件触发
/**
* 1. 三个监听器的目的相同, 最终目的都是进行 事件收集、事件调用 (都会调用 dispatchEvent)
* 2. 不同在于监听器在调用 dispatchEvent 之前发生的事情不一样
* - dispatchEvent: 连续事件或其他事件监听器 (第三类监听器), 由于其优先级最高, 是直接同步调用的, 而另外两类不同
* - dispatchUserBlockingUpdate (用户阻塞事件监听器): 内部会判断暂时跳过调用
* - dispatchDiscreteEvent (离散事件监听器):
*/
/**
* dispatchDiscreteEvent | dispatchUserBlockingUpdate | dispatchEvent
* 前三个参数由当前函数提供: domEventName, eventSystemFlags, targetContainer
* 最后一个参数便是原生监听器会拥有的唯一入参 Event 对象: nativeEvent
*/
return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}
- 无论是 dispatchDiscreteEvent | dispatchUserBlockingUpdate | dispatchEvent, 原生事件执行时候触发: 最终执行的是 dispatchEvent 事件触发, 这个下面会讲到
- 三个监听器的目的相同, 最终目的都是进行 事件收集、事件调用 (都会调用 dispatchEvent)
- 不同在于监听器在调用 dispatchEvent 之前发生的事情不一样
- dispatchEvent: 连续事件或其他事件监听器 (第三类), 由于其优先级最高, 是直接同步调用的, 而另外两类不同
- dispatchUserBlockingUpdate (用户阻塞事件监听器(第二类)): 内部根据调度优先级会判断结束事件调用 还是 运行事件
- dispatchDiscreteEvent (离散事件监听器(第一类)): 调用和 dispatchUserBlockingUpdate 相同, 调度优先级不同
- dispatchDiscreteEvent | dispatchUserBlockingUpdate | dispatchEvent 参数
- 前三个参数由当前函数提供:
- domEventName
- eventSystemFlags
- targetContainer
- 最后一个参数便是原生监听器会拥有的唯一入参 Event 对象: nativeEvent
- 前三个参数由当前函数提供:
总结
- 这里重点主要做一件事, 将React 支持的原生事件 通过
addEventListener
标准事件模式, 委托事件的 冒泡和捕获 阶段事件绑定到root
节点上- 委托 容器 root 节点上
- 委托 容器 root 节点上
不可代理事件绑定: 代理监听事件在自身元素 上
非代理事件 nonDelegatedEvents, 这些事件不存在冒泡阶段, 在root代理冒泡阶段监听器也不会触发, 需特殊处理;
代理发生在 DOM 实例的创建阶段 , render 阶段的 completeWork 阶段, 通过调用 finalizeInitialChildren 为DOM实例设置属性时, 判断 DOM 节点类型来添加对应的 监听器
- 如为
<img />
和<link />
标签对应的DOM实例添加error
和load
的监听器
/**
* 完成初始子项
* @param {*} domElement element
* @param {*} type element type (button, div, ....)
* @param {*} props props
* @param {*} rootContainerInstance 根节点实例
* @param {*} hostContext
* @returns
*/
function finalizeInitialChildren(domElement, type, props, rootContainerInstance, hostContext) {
setInitialProperties(domElement, type, props, rootContainerInstance);
// ...
}
function setInitialProperties(domElement, tag, rawProps, rootContainerElement) {
var isCustomComponentTag = isCustomComponent(tag, rawProps);
var props;
switch (tag) {
case 'dialog':
listenToNonDelegatedEvent('cancel', domElement);
listenToNonDelegatedEvent('close', domElement);
// ...
break;
case 'iframe':
case 'object':
case 'embed':
// ...
listenToNonDelegatedEvent('load', domElement);
// ...
break;
case 'video':
case 'audio':
// ...
for (var i = 0; i < mediaEventTypes.length; i++) {
listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
}
// ...
break;
case 'source':
// ...
listenToNonDelegatedEvent('error', domElement);
// ...
break;
case 'img':
case 'image':
case 'link':
// ...
listenToNonDelegatedEvent('error', domElement);
listenToNonDelegatedEvent('load', domElement);
// ...
break;
case 'details':
// ...
listenToNonDelegatedEvent('toggle', domElement);
// ...
break;
case 'input':
// ...
listenToNonDelegatedEvent('invalid', domElement);
break;
// ...
case 'select':
// ...
listenToNonDelegatedEvent('invalid', domElement);
break;
case 'textarea':
// ...
listenToNonDelegatedEvent('invalid', domElement);
break;
default:
props = rawProps;
}
// ...
// scroll 事件处理
setInitialDOMProperties(tag, domElement, rootContainerElement, props, isCustomComponentTag);
// ....
}
function setInitialDOMProperties(tag, domElement, rootContainerElement, nextProps, isCustomComponentTag) {
// ...
for (var propKey in nextProps) {
if (!nextProps.hasOwnProperty(propKey)) {
continue;
}
var nextProp = nextProps[propKey];
// ...
else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
}
// ...
}
}
/**
* 非代理事件监听器绑定 绑定冒泡阶段名称 (模拟冒泡)
* @param {*} domEventName : 事件名称
* @param {*} targetElement : 绑定元素
*/
function listenToNonDelegatedEvent(domEventName, targetElement)
// 绑定在目标冒泡阶段
var isCapturePhaseListener = false;
var listenerSet = getEventListenerSet(targetElement);
var listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener);
if (!listenerSet.has(listenerSetKey)) {
// 根据事件优先级, 绑定代理监听器到 目标 元素
addTrappedEventListener(targetElement, domEventName, IS_NON_DELEGATED, isCapturePhaseListener);
listenerSet.add(listenerSetKey);
}
}
- 将不能冒泡的事件, 事件监听绑定在 元素 本身, 只注册冒泡阶段触发的监听
- 绑定元素节点
- 绑定元素节点
到此直到页面渲染完后成, 合成事件系统基本没有其他事了, 渲染完成后, 当浏览器发生了原生事件的调用, 合成事件系统才会开始工作, 监听器会接收浏览器发生的事件, 然后向下传递信息, 收集相关事件, 模拟浏览器的触发流程并达成我们预期的触发效果。
转载自:https://juejin.cn/post/7361358666212687881