在一文帮你熟悉 React17 事件机制 (一)文档中我们了解了React
事件机制的前两个阶段 框架初始化: 事件初始化注册 和 监听代理 React 支持的所有原生事件: 事件代理, 接下来我们了解下React
的合成事件系统触发如何实现的?即React
事件机制的最后一个阶段 触发事件: 事件收集 & 处理事件
触发事件: 事件收集 & 处理事件
为了方便理解, 我们介绍下面事件触发, 用click
事件举例子, 当页面中有个button
按钮, 我们触发按钮点击。
流程如下:
- 当点击
button
按钮时候, 当前事件流是捕获阶段,window
到div#root
事件委托节点, 由于在此绑定事件捕获监听, 在此节点触发 click 捕获事件函数触发, 执行React 事件执行处理机制 (这里后面讲解) - 事件处理完之后, 继续捕获, 一直到目标阶段, 找到
button
元素, 由于容器节点下是没有任何事件的, 下来执行事件流冒泡阶段 - 从
button
元素一直往上找, 当到div#root
事件委托节点时候, 由于在此绑定事件冒泡监听, 在此节点触发click
冒泡事件函数触发, 执行React 事件处理 (这里后面讲解) - 事件处理完之后, 继续冒泡, 一直到
window
, 中间如果还有事件, 执行事件 React
事件处理还是基于js事件流
处理流程, 在委托节点 捕获 和 冒泡 处理里面执行自己逻辑, 即React 事件执行处理- 遇到阻止事件传播执行其逻辑:
React里面使用 e.stopPropagation() 阻止事件传播
阻止事件传播 流程如下:
- 捕获阶段 & 冒泡阶段 遵循
JS事件机制
- React 事件处理 由于委托在
容器节点上, 在容器节点元素这里 阻止后续 JS 事件机制执行
- React 事件系统 内部
虚拟冒泡收集事件队列, 执行队列时候阻止后续事件执行
下面是一个 触发不同位置 阻止事件传播 例子 点击内容: 分别在 位置 1, 2, 3, 4 触发阻止事件传播。
<div id="container"></div>
<script>
function Content() {
React.useEffect(() => {
const id = document.getElementById('ele-id');
const handleDocumentClick = (e) => {
console.log('[原生] click @document');
};
const handleDocumentClickCapture = (e) => {
console.log('[原生] click @document (capture 阶段)');
};
const handleClick = (e) => {
console.log('[原生] click $div#ele-id 元素');
// 位置3: e.stopPropagation();
};
const handleClickCapture = (e) => {
console.log('[原生] click $div#ele-id 元素 (capture 阶段)');
// 位置4: e.stopPropagation();
};
document.addEventListener('click', handleDocumentClick);
document.addEventListener('click', handleDocumentClickCapture, true);
id.addEventListener('click', handleClick);
id.addEventListener('click', handleClickCapture, true);
return () => {
document.removeEventListener('click', handleDocumentClick);
id.removeEventListener('click', handleClick);
id.removeEventListener('click', handleClickCapture, true);
};
}, []);
return (
<div
onClick={(e) => {
console.log('[React] click @Content 组件');
// 位置1: e.stopPropagation();
}}
onClickCapture={(e) => {
console.log('[React] click @Content 组件 (capture 阶段)');
// 位置2: e.stopPropagation();
}}
>
<div id="ele-id">
<button
onClick={() => {
console.log('[React] click $button 元素');
}}
>
{'点击内容'}
</button>
</div>
</div>
);
}
function App() {
return (
<div
onClick={(e) => {
console.log('[React] click @App 组件');
}}
onClickCapture={(e) => {
console.log('[React] click @App 组件 (capture 阶段)');
}}
>
<Content />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('container'), () => {}
);
</script>
- 不添加任何 阻止事件传播
[原生] click @document (Capture 阶段)
[React] click @App 组件
[React] click @Content 组件 (capture 阶段)
[原生] click $div#ele-id 元素 (capture 阶段)
[原生] click $div#ele-id 元素
[React] click $button 元素
[React] click @Content 组件
[React] click @App 组件
[原生] click @document
-
位置1: [React] Content 组件 onClick 添加 e.stopPropagation()
- 阻止冒泡阶段 组件Content真实事件 后的事件执行
- 委托给 容器, 阻止位置在div#container 后面 冒泡执行: document (即 body -> document -> window)
- div#ele-id 元素绑定监听原生事件, 冒泡先冒泡此元素, 因此会先执行
[原生] click @document (capture 阶段)
[React] click @App 组件 (capture 阶段)
[React] click @Content 组件 (capture 阶段)
[原生] click $div#ele-id 元素 (capture 阶段)
[原生] click $div#ele-id 元素
[React] click $button 元素
[React] click @Content 组件
- 位置2:
[React] Content 组件
onClickCapture
添加e.stopPropagation()
- 阻止捕获阶段
组件Content
React事件
后的事件执行, 后续React捕获事件
被阻止 - 委托给 容器, 阻止位置在
div#container
后面 捕获 + 冒泡 都被阻止
- 阻止捕获阶段
[原生] click @document (Capture 阶段)
[React] click @App 组件 (capture 阶段)
[React] click @Content 组件 (capture 阶段)
- 位置3:
[原生] $div#ele-id 元素
click冒泡
添加e.stopPropagation()
- 阻止冒泡阶段
div#ele-id
元素 后的冒泡事件传播- 委托给 容器, 阻止位置在
div#container
,div#ele-id
冒泡阶段的 后续都被阻止
- 委托给 容器, 阻止位置在
- 阻止冒泡阶段
[原生] click @document (Capture 阶段)
[React] click @App 组件 (capture 阶段)
[React] click @Content 组件 (capture 阶段)
[原生] click $div#ele-id 元素 (capture 阶段)
[原生] click $div#ele-id 元素
- 位置4:
[原生] $div#ele-id 元素
click捕获
添加e.stopPropagation()
- 阻止捕获阶段
div#ele-id
元素 后的捕获 + 冒泡事件传播 - 委托给 容器,
div#container
在div#ele-id
捕获阶段的前面, 因此React 捕获事件
被执行
- 阻止捕获阶段
[原生] click @document (Capture 阶段)
[React] click @App 组件 (capture 阶段)
[React] click @Content 组件 (capture 阶段)
[原生] click $div#ele-id 元素 (capture 阶段)
React 事件处理
点击元素, 根据 捕获阶段 和 冒泡阶段 处理委托节点对应函数执行, 那么函数执行中 React做了什么呢? 我们接下来揭秘 执行React 事件处理。
下图是React
事件触发 流程图, 经历 捕获阶段 -> 冒泡阶段, 处理 React 事件 和 原生事件
的基本流程:
执行委托处理函数: React 虚拟冒泡收集 React 处理事件, 收集之后执行函数
前面讲到, 我们根据事件优先级委托绑定事件监听器有三种, 分别为
- 离散事件监听器(dispatchDiscreteEvent)
- 用户阻塞事件监听器(dispatchUserBlockingUpdate)
- 连续事件及其他事件监听器(dispatchEvent)
离散事件 和 用户阻塞事件监听器 最终还是调用 dispatchEvent我们简单看下 dispatchDiscreteEvent 和 dispatchUserBlockingUpdate
用户阻塞事件监听器: 第二类 dispatchUserBlockingUpdate (非重点)
/*
* 用户阻塞事件监听器: 简化版本
* 前三个参数是在注册事件代理的时候便传入的
* domEventName:对应原生事件名称
* eventSystemFlags:本文范文内其值仅有可能为 4 或者 0,分别代表 捕获阶段事件 和 冒泡阶段事件
* container:应用根 DOM 节点
* nativeEvent:原生监听器传入的 Event 对象
*/
function dispatchUserBlockingUpdate(domEventName, eventSystemFlags, container, nativeEvent) {
/**
* runWithPriority => Scheduler_runWithPriority => Scheduler.unstable_runWithPriority
* UserBlockingPriority 任务优先级 2
* dispatchEvent.bind(...): 执行的任务
* - 前三个参数是在注册事件代理的时候便传入的,
domEventName:对应原生事件名称
eventSystemFlags:这里执行其值仅有可能为 4 或者 0,分别代表 捕获阶段事件 和 冒泡阶段事件
container:应用根 DOM 节点
nativeEvent:原生监听器传入的 Event 对象
runWithPriority 里面会执行 第二个函数
runWithPriority(priority, fn) {
fn();
}
*/
runWithPriority(
UserBlockingPriority, // 2
dispatchEvent.bind(null, domEventName, eventSystemFlags, container, nativeEvent)
);
}
- 调用 unstable_runWithPriority 调度函数, 优先级不够时候阻止事件运行, 否则直接运行 dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent)
runWithPriority 处理代码
function runWithPriority(reactPriorityLevel, fn) {
// React优先级 获取 调度优先级
var priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
// unstable_runWithPriority
return Scheduler_runWithPriority(priorityLevel, fn);
}
var Scheduler_runWithPriority = Scheduler.unstable_runWithPriority;
/**
* 调度 执行函数:
* - 优先级不够时候, 直接不执行
* - 否则 直接运行 函数
*/
function unstable_runWithPriority(priorityLevel, eventHandler) {
switch (priorityLevel) {
case ImmediatePriority:
case UserBlockingPriority:
case NormalPriority:
case LowPriority:
case IdlePriority:
break;
default:
priorityLevel = NormalPriority;
}
// ...
try {
return eventHandler();
} finally {
// ...
}
}
离散事件监听器 : 第一类 dispatchDiscreteEvent (非重点)
// 离散事件监听器 简化版本
function dispatchDiscreteEvent(domEventName, eventSystemFlags, container, nativeEvent) {
// ...
// 新建一个离散更新
// 实际: 后面四个参数实际上第一个函数的参数
// - dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent)
discreteUpdates(
dispatchEvent,
domEventName,
eventSystemFlags,
container,
nativeEvent
);
}
function discreteUpdates(fn, a, b, c, d) {
// ...
try {
// 调用 Scheduler 里的离散更新函数: 看 discreteUpdates$1 => unstable_runWithPriority
// 最终和 dispatchUserBlockingUpdate 调用一致
return discreteUpdatesImpl(fn, a, b, c, d);
} finally {
// ...
}
}
- 离散事件监听器 执行的是 discreteUpdatesImpl 函数
- discreteUpdatesImpl 经过一系列函数处理, 最终执行的是 fn(a, b, c, d)
- fn(a, b, c, d) 就是 dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent)
我们来看看 discreteUpdatesImpl 具体执行的是什么?
- 生成新函数
func = fn.bind(null, a, b, c, d)
, 即如下- func = dispatchEvent.bind(null, domEventName, eventSystemFlags, container, nativeEvent)
- discreteUpdatesImpl 执行和 用户阻塞事件监听器 (第二类) 调用同一个函数
/**
* discreteUpdatesImpl: 离散更新函数
*/
discreteUpdatesImpl = function discreteUpdates(fn, a, b, c, d) {
// ...
try {
/**
* 这里和第二类监听处理就一模一样
* runWithPriority => Scheduler_runWithPriority => Scheduler.unstable_runWithPriority
* UserBlockingSchedulerPriority: 任务优先级 98
* fn.bind(null, a, b, c, d) => func: func() 执行不多做赘述
*/
return runWithPriority(
UserBlockingSchedulerPriority, // 98
fn.bind(null, a, b, c, d)
);
} finally {
// ...
}
}
看过**离散事件监听器
** 和 用户阻塞事件监听器
, 我们可以了解这两种事件处理 最后 执行的是 第三类连续事件或其他事件监听器 dispatchEvent
连续事件或其他事件监听器: 第三类 dispatchEvent (重点)
dispatchEvent 代码: 简化版本
// 尝试调度一个事件. 如果被阻止, 则返回 SuspenseInstance 或 Container
function attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {
// 获取原生dom节点: 获取监听器中传进来的 Event 对象, 并获取 nativeEvent.target 内的 DOM 节点
var nativeEventTarget = getEventTarget(nativeEvent);
// 获取原生dom节点对应的fiber
var targetInst = getClosestInstanceFromNode(nativeEventTarget);
// 下面 判断fiber节点的类型以及是否已渲染来决定是否要派发事件
// 得到的 Fiber 实例可能有一些问题, 不是想要的, 这里做兼容, 可忽略
// ...
dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer);
return null;
}
/**
*
* 当原生事件被触发后, 进入dispatchEvent
* 调用 dispatchEventsForPlugins: 这个函数触发了 事件收集、事件执行
* @param {*} domEventName : DOM 事件名称 click
* @param {*} eventSystemFlags : 事件系统标记
* @param {*} targetContainer : root DOM 元素
* @param {*} nativeEvent : 原生事件 (from: addEventListener)
* @returns
*/
function dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {
// ...
// 尝试调度事件
// - 如果被阻止, 则返回 SuspenseInstance 或 Container, 拿到原生DOM节点或该节点对应的fiber
// = 如果调度, 直接运行 dispatchEventForPluginEventSystem, 返回 null
var blockedOn = attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent);
if (blockedOn === null) {
// ...
return;
}
// ...
// 通过插件系统,触发事件
dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, null, targetContainer);
}
- 上面经过处理, 就是执行 dispatchEventForPluginEventSystem函数
- attemptToDispatchEvent 这里我们还能了解到, React fiber 和 dom 实例如何建立关联
dispatchEventForPluginEventSystem
- 这个函数做一件事: 执行 dispatchEventsForPlugins
/**
* 当页面上触发了特定的事件时, 如点击事件click, 就会触发绑定在根元素上的事件回调函数
* 即之前绑定了参数的dispatchEvent, 在内部最终会调用dispatchEventsForPlugins
* @param {*} domEventName DOM 事件名称 click
* @param {*} eventSystemFlags : 事件系统标记
* @param {*} nativeEvent : 原生事件
* @param {*} targetInst : Fiber, 点击元素的Fiber
* @param {*} targetContainer: 容器节点DOM
* @returns
*/
function dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
var ancestorInst = targetInst;
// ...
/**
* fn = function () {
return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst);
}
* 这个是偏函数 就是函数调用, 执行 batchedEventUpdates(fn, a, b) => fn(a) => dispatchEventsForPlugins(...)
* 最终执行 dispatchEventsForPlugins
*/
batchedEventUpdates(function () {
return dispatchEventsForPlugins(
domEventName, eventSystemFlags, nativeEvent, ancestorInst
);
});
}
var batchedEventUpdatesImpl = batchedUpdatesImpl = function(fn, bookkeeping) {
return fn(bookkeeping);
}
function batchedEventUpdates(fn, a, b) {
// 是否在事件执行批次中
if (isBatchingEventUpdates) {
return fn(a, b);
}
isBatchingEventUpdates = true;
try {
// 执行函数
// batchedEventUpdatesImpl => batchedUpdatesImpl =>(fn, a) => fn(a)
return batchedEventUpdatesImpl(fn, a, b);
} finally {
isBatchingEventUpdates = false;
finishEventHandler();
}
}
dispatchEventsForPlugins
- React 事件函数收集 + 函数处理
- 收集是根据事件的类型分别处理的,
extractEvents
的入参分别给各个事件处理插件的extractEvents
进行分别处理 processDispatchQueue
处理收集函数- 捕获阶段 事件队列后序遍历执行
- 冒泡阶段 事件队列顺序遍历执行
- 原因: 事件收集是 从目标元素 冒泡 到 root 收集的 事件队列
/**
* 冒泡 收集函数 + 批处理函数
* @param {*} domEventName : dispatchEvent中绑定的事件名 click
* @param {*} eventSystemFlags : dispatchEvent绑定的事件标记
* @param {*} nativeEvent : 事件触发时回调传入的原生事件对象
* @param {*} targetInst : 事件触发目标元素对应的fiber
* @param {*} targetContainer : (一般undefined, 暂时没找到哪里传)
*/
function dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer
) {
// 获取了一遍 event.target dom
var nativeEventTarget = getEventTarget(nativeEvent);
// 事件队列, 收集到的事件都会存储到这
var dispatchQueue = [];
// 下面两个是重点 收集 & 处理
// 收集事件: 进行事件合成
// 这里源码执行的是一个处理集合:
/*
这里源码执行的是一个处理集合:
{
// extractEvents$4
SimpleEventPlugin.extractEvents(...); // 大部分事件收集
// mouseover, mouseout, pointerover, pointerout 处理
// extractEvents$2
EnterLeaveEventPlugin.extractEvents(...);
// extractEvents$1
ChangeEventPlugin.extractEvents(...); // input, textarea, select 相关的事件 处理
// focusin, focusout, mousedown, contextmenu, mouseup,
// dragend, selectionchange, keydown, keyup 事件处理
// extractEvents$3
SelectEventPlugin.extractEvents(...);
// compositionEvent: compositionend, compositionstart, compositionupdate
// beforeInputEvent: beforeinput 处理
// extractEvents
BeforeInputEventPlugin.extractEvents(...);
}
直接看这个 impleEventPlugin.extractEvents (extractEvents$4) 就可以
*/
extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
// 执行事件: 根据事件阶段(冒泡或捕获)来决定是正序还是倒序遍历合成事件中的listeners
/**
* dispatchQueue 存储: 单个类型事件会存在一个单元
* [
* {
* event: click 事件对象,
* listeners: [{handler}, ...], 所有冒泡 (捕获) 上元素事件集合
* },
* {
* event: change 事件对象,
* listeners: [{handler}, ...], 所有冒泡 (捕获)上元素change事件集合
* },
* ....
* ]
*/
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
extractEvents
的内容其实很简单, 按需调用几个 EventPlugin
的 extractEvents
, 这几个 extractEvents
的目的是一样的, 只不过针对不同的事件可能会生成不同的事件, 我们以最核心的也是最关键的 SimpleEventPlugin.extractEvents
来讲解
SimpleEventPlugin.extractEvents 事件函数收集
click 为例子, click 合成事件的构造函数 SyntheticEvent
- accumulateSinglePhaseListeners 获取当前事件的 所有react 事件函数, 返回 函数队列:
- listeners = [{instance, listener, currentTarget}, ...]
- event = new SyntheticEventCtor (SyntheticEvent) 生成合成事件的 Event 对象
- 抹平浏览器之间差异
- 关键方法的包装
- 最终返回需要执行事件队列 dispatchQueue: [{event, listeners}, ...]
/**
* 收集
* 先执行捕获事件, 在执行冒泡事件
* onClick: 冒泡事件
* onClickCapture: 捕获事件
* 都是根据触发节点向上匹配
* - 先匹配某个类型事件(click)所有捕获事件集合, 派发执行
* - 再匹配某个类型事件(click)所有冒泡事件集合, 派发执行
* @param {*} dispatchQueue : 事件队列
* @param {*} domEventName : 事件名称
* @param {*} targetInst : 目标元素 fiber
* @param {*} nativeEvent : 原生事件对象
* @param {*} nativeEventTarget
* @param {*} eventSystemFlags : 系统标识
* @param {*} targetContainer : undefined
* @returns
*/
function extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
// 根据原生事件名称获取合成事件名称
var reactName = topLevelEventsToReactNames.get(domEventName);
if (reactName === undefined) {
return;
}
// 默认合成函数的构造函数
var SyntheticEventCtor = SyntheticEvent;
var reactEventType = domEventName;
// 按照原生事件名称来获取对应的合成事件构造函数
switch (domEventName) {
/*
SyntheticEventCtor = SyntheticKeyboardEvent;
SyntheticEventCtor = SyntheticFocusEvent;
SyntheticEventCtor = SyntheticFocusEvent;
SyntheticEventCtor = SyntheticFocusEvent;
SyntheticEventCtor = SyntheticMouseEvent;
SyntheticEventCtor = SyntheticDragEvent;
SyntheticEventCtor = SyntheticTouchEvent;
SyntheticEventCtor = SyntheticAnimationEvent;
SyntheticEventCtor = SyntheticTransitionEvent;
SyntheticEventCtor = SyntheticUIEvent;
SyntheticEventCtor = SyntheticWheelEvent;
SyntheticEventCtor = SyntheticClipboardEvent;
SyntheticEventCtor = SyntheticPointerEvent;
*/
}
// 是否是捕获阶段
var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
// scroll 事件不冒泡
var accumulateTargetOnly =!inCapturePhase && domEventName === 'scroll';
// 获取当前阶段的所有事件
var _listeners = accumulateSinglePhaseListeners(targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly);
if (_listeners.length > 0) {
// 生成合成事件的 Event 对象: click
// 不同类型处理不一样
var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
// 入队
dispatchQueue.push({
event: _event,
listeners: _listeners
});
}
}
- accumulateSinglePhaseListeners: 累计
click
事件当前阶段 (冒泡 或者 捕获) 所有事件- 捕获阶段收集 onClickCapture 函数
- 冒泡阶段收集 onClick 函数
- 无论冒泡还是捕获阶段, React 事件收集从事件触发目标元素
fiber
冒泡 到root fiber
, 收集fiber props 里面的对应事件函数 (React 虚拟冒泡) - 返回 事件函数集合: [{instance, listener, lastHostComponent}, ....]
- Instance: 目标元素fiber, 事件处理中不会变,
event.target 对应元素Fiber
- Listener: React 事件函数, 例如 onClick (fn)
- currentTarget: 绑定React事件 的dom 元素
- Instance: 目标元素fiber, 事件处理中不会变,
/**
* 累计click事件当前阶段 (冒泡 或者 捕获) 所有事件
* - 捕获阶段收集 onClickCapture 函数
- 冒泡阶段收集 onClick 函数
- 无论冒泡还是捕获阶段, React 事件收集从事件触发目标元素 fiber 冒泡 到root fiber,
收集fiber props 里面的对应事件函数 (React 虚拟冒泡)
* @param {*} targetFiber : 目标fiber
* @param {*} reactName : React事件名称 onClick
* @param {*} nativeEventType : 原生事件名称(类型即名称) click
* @param {*} inCapturePhase : 是否捕获阶段 调用的委托事件触发器
* @param {*} accumulateTargetOnly
* @returns
*/
function accumulateSinglePhaseListeners(
targetFiber, reactName, nativeEventType, inCapturePhase, accumulateTargetOnly
) {
// 捕获函数名称 onClickCapture
var captureName = reactName !== null ? reactName + 'Capture' : null;
// 最终合成事件名称
var reactEventName = inCapturePhase ? captureName : reactName;
// 事件函数 队列
var listeners = [];
// 目标元素 fiber
var instance = targetFiber;
// 这个收集 当前React事件绑定的元素
// 还记得之前说的 事件绑定元素 和 目标元素么?
var lastHostComponent = null;
// 从目标元素开始一直到root,累加所有的fiber对象和事件监听
while (instance !== null) {
var _instance2 = instance,
// fiber 对应的 dom 元素, fiber.stateNode
stateNode = _instance2.stateNode,
tag = _instance2.tag; // Handle listeners that are on HostComponents (i.e. <div>)
// 有效节点则获取其事件
if (tag === HostComponent && stateNode !== null) {
// 这里设置 当前 dom 元素
lastHostComponent = stateNode;
if (reactEventName !== null) {
// 获取存储在 Fiber 节点上 Dom Props 里的对应事件
// instance: fiber.stateNode[internalPropsKey][reactEventName]
var listener = getListener(instance, reactEventName);
if (listener != null) {
// 入对
// 将返回 {instance, listener, currentTarget: lastHostComponent} 对象 入队
listeners.push(
/**
* 简单返回一个 当前事件 {instance, listener, currentTarget: lastHostComponent} 对象
* instance: fiber
* listener: 事件处理函数
* currentTarget: dom 绑定函数的dom, 事件需要指向currentTarget
*/
createDispatchListener(
instance, listener, lastHostComponent
)
);
}
}
}
// scroll 不会冒泡, 获取一次就结束了
if (accumulateTargetOnly) {
break;
}
// 其父级 Fiber 节点, 向上递归
instance = instance.return;
}
// 该事件名(如click) 对应收集的监听器(执行函数)
return listeners;
}
- new SyntheticEventCtor: SyntheticEvent = createSyntheticEvent(EventInterface)
- 根据事件类型对原生事件的属性做浏览器的抹平
- 关键方法的包装
// 辅助函数,永远返回true
function functionThatReturnsTrue() { return true;}
// 辅助函数,永远返回false
function functionThatReturnsFalse() { return false;}
var EventInterface = {
eventPhase: 0,
bubbles: 0,
cancelable: 0,
timeStamp: function (event) {
return event.timeStamp || Date.now();
},
defaultPrevented: 0,
isTrusted: 0
};
// 这个其实就是 SyntheticBaseEvent 函数
// 使用通过给工厂函数传入事件接口获取事件合成事件构造函数
var SyntheticEvent = createSyntheticEvent(EventInterface);
function createSyntheticEvent(Interface) {
// 合成事件构造函数
function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
// react事件名
this._reactName = reactName;
// 当前执行事件回调时的 fiber (传入的是null)
this._targetInst = targetInst;
// 真实事件名
this.type = reactEventType;
// 原生事件对象
this.nativeEvent = nativeEvent;
// 原生触发事件的DOM target
this.target = nativeEventTarget;
// 当前执行回调的DOM
this.currentTarget = null;
/**
* 抹平字段在浏览器间的差异
* 内部要么函数 & 要么是属性 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];
}
}
// 抹平defaultPrevented的浏览器差异,即抹平e.defaultPrevented和e.returnValue的表现
var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;
if (defaultPrevented) {
// 在处理事件时已经被阻止默认操作了, 调用isDefaultPrevented一直返回true
this.isDefaultPrevented = functionThatReturnsTrue;
} else {
// 在处理事件时没有被阻止过默认操作, 则先用返回false的函数
this.isDefaultPrevented = functionThatReturnsFalse;
}
// 默认执行时, 还没有被阻止继续传播, 调用isPropagationStopped返回false
this.isPropagationStopped = functionThatReturnsFalse;
return this;
}
_assign(SyntheticBaseEvent.prototype, {
// 阻止默认事件
preventDefault: function () {
// 调用后设置 defaultPrevented
this.defaultPrevented = true;
var event = this.nativeEvent;
if (!event) {
return;
}
// e.preventDefault() 和 e.returnValue=false 的浏览器差异, 并在原生事件上执行
if (event.preventDefault) {
event.preventDefault(); // $FlowFixMe - flow is not aware of `unknown` in IE
} else if (typeof event.returnValue !== 'unknown') {
event.returnValue = false;
}
// 后续回调判断时都会返回true
this.isDefaultPrevented = functionThatReturnsTrue;
},
// 阻止捕获和冒泡阶段中当前事件的进一步传播
stopPropagation: function () {
var event = this.nativeEvent;
if (!event) {
return;
}
// e.stopPropagation() 和 e.calcelBubble = true的差异, 并在原生事件上执行
if (event.stopPropagation) {
event.stopPropagation(); // $FlowFixMe - flow is not aware of `unknown` in IE
} else if (typeof event.cancelBubble !== 'unknown') {
event.cancelBubble = true;
}
// 然后后续判断时都会返回true, 停止传播
this.isPropagationStopped = functionThatReturnsTrue;
},
// 合成事件不使用对象池了, 这个事件是空的没有意义
persist: function () {},
isPersistent: functionThatReturnsTrue
});
return SyntheticBaseEvent;
}
事件收集在这里就执行完了, 我们用下面图表示下流程
processDispatchQueue: 处理收集函数
- processDispatchQueue: 遍历事件队列
- 队列每个单元对应的是一类事件 所有事件, 因此需要遍历执行
function processDispatchQueue(dispatchQueue, eventSystemFlags) {
// 通过 eventSystemFlags 判断当前事件阶段
// 是否捕获阶段
var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
// 遍历合成事件
for (var i = 0; i < dispatchQueue.length; i++) {
// 取出其合成 Event 对象及事件集合
var _dispatchQueue$i = dispatchQueue[i],
event = _dispatchQueue$i.event,
listeners = _dispatchQueue$i.listeners;
// 这个函数就负责事件的调用
// 如果是捕获阶段的事件则倒序调用, 反之为正序调用, 调用时会传入合成 Event 对象
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
}
// 错误抛出
rethrowCaughtError();
}
- processDispatchQueueItemsInOrder: 根据不同阶段处理事件的调用
- 捕获阶段: 后序遍历处理执行
- 冒泡阶段: 顺序遍历处理执行
- 如果中间有函数里面调用 e.stopPropagation() 阻止事件执行
- onClick = e => e.stopPropagation()
- event.isPropagationStopped() => functionThatReturnsTrue = () => true 返回true
- 后面函数执行就不会进行了
// 负责事件的调用
function processDispatchQueueItemsInOrder(event, dispatchListeners, inCapturePhase) {
var previousInstance;
if (inCapturePhase) {
for (var i = dispatchListeners.length - 1; i >= 0; i--) {
var _dispatchListeners$i = dispatchListeners[i],
instance = _dispatchListeners$i.instance,
currentTarget = _dispatchListeners$i.currentTarget,
listener = _dispatchListeners$i.listener;
// onClick = e => e.stopPropagation()后
// event 的 isPropagationStopped() => functionThatReturnsTrue = () => true
// 那么就不会在调用后面的listener了,下面的冒泡同理
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
// 和上面一样, 不过是 for (var _i = 0; _i < dispatchListeners.length; _i++)
// 顺序执行
}
}
- executeDispatch: 函数执行
- 由于合成事件对象时候 currentTarget 是null, 调用函数时候保证能感知绑定元素, 需要 给event.currentTarget 赋值
- 执行完成后需要清空 event.currentTarget
function executeDispatch(event, listener, currentTarget) {
var type = event.type || 'unknown-event';
/**
* 这一步是必须的, 保证事件对象的完整性
* currentTarget: 正在执行的监听函数所绑定的那个节点
*/
// 设置合成事件执行到当前DOM实例时的指向
event.currentTarget = currentTarget;
// (name, func, context, ....arg)
// var funcArgs = Array.prototype.slice.call(arguments, 3);
// func.apply(context, funcArgs); => listener.apply(undefined, event)
// 运行函数 且 错误收集
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
// 不在事件的回调中时拿不到currentTarget
event.currentTarget = null;
}
下次事件重新触发: 重新走捕获冒泡流程, React 事件机制重新收集 + 处理
- React 事件每次执行为什么要重新收集, 没有缓存?
- 每一次事件触发后, 原有的树结构发生改变, 历史收集的 事件有可能已经不存在
- 不同触发期间 事件函数有可能根据状态的变化 发生改变, 此时旧事件就无效了
- 由于闭包, React 事件函数本身是一个闭包, 当运行缓存函数时候, 存储的状态不是最新的, 此时事件处理状态不是最新的
自此, 事件机制就结束了, 总结下来:
React
合成事件系统其实并不算特别复杂, 其核心思想就是用事件代理统一接收事件的触发, 然后由React
自身来调度真实事件的调用, 而React
如何知道应该从哪开始收集事件的核心其实是存储在真实DOM
上的Fiber
节点, 从真实穿梭到虚拟。React 17
利用原生 捕获和冒泡 事件的支持, 对齐了浏览器原生标准, 同时 onScroll 事件不再进行事件冒泡, onFocus 和 onBlur 使用原生 focusin & focusout 合成。React
中事件触发的本质是对dispatchEvent
函数的调用, 模拟原生的事件的捕获和冒泡, 监听委托事件执行, 根据目标元素Fiber 树, 冒泡收集事件, 顺序执行。