Эта статья основана на React v16.5.2 для анализа исходного кода.
Основной процесс
В начале файла react-dom/src/events/ReactBrowserEventEmitter.js в исходниках реакции есть такой большой комментарий:
Делегирование событий — очень распространенная стратегия оптимизации событий браузера, поэтому React взял на себя это дело, а также тщательно устранил различия между браузерами, предоставив разработчикам опыт кроссбраузерной разработки, в основном с помощью EventPluginHub.Things отвечает за планирование хранения событий, синтезирование событий, их создание и уничтожение в виде пулов объектов.Что касается следующей структурной схемы, то это графическое описание механизма событий.
- События React используют механизм делегирования событий. Как правило, функция делегирования событий состоит в том, чтобы уменьшить количество зарегистрированных событий на странице, уменьшить нагрузку на память и оптимизировать производительность браузера. React также имеет такую цель. чтобы иметь возможность Лучше управлять событиями, фактически все события в React окончательно делегируются в DOM верхнего уровня документа
- Поскольку все события делегируются документу, должен существовать механизм управления, и все события инициируются и вызываются в порядке очереди «первым поступил — первым обслужен».
- Поскольку вы взяли через мероприятие, то у вас нет каких-либо отходов событий, поэтому у вас есть собственное синтетическое событие (синтетическийВЕНТ), синтетическое событие отвечает за синтезирование, различные типы событий различными плагином. Синтез, такой как SimpleEvent Plugin, Plugion Tapevent и т. Д.
- Чтобы еще больше повысить производительность событий, EventPluginHub отвечает за создание и уничтожение искусственных объектов событий.
#Начинать
<button onClick={this.autoFocus}>点击聚焦</button>
Это обычный способ связывания событий в React. При анализе JSX кнопка будет смонтирована как компонент. А onClick на данный момент является обычным реквизитом. Когда ReactDOMComponent загружает компоненты (mountComponent) и обновляет (updateComponent), ему необходимо обработать реквизиты (_updateDOMProperties):
регистрация на мероприятие
ReactDOMComponent.Mixin = {
mountComponent:function(){},
_createOpenTagMarkupAndPutListeners:function(){},
....,
// 方法中有指向上次属性值得lastProp,
// nextProp是当前属性值,这里nextProp是我们绑定给组件的onclick事件处理函数。
// nextProp 不为空调用enqueuePutListener绑定事件,为空则注销事件绑定。
_updateDOMProperties:function(lastProps, nextProps, transaction){
for (propKey in lastProps) {} //省略。。。
for (propKey in nextProps) {
// 判断是否为事件属性
if (registrationNameModules.hasOwnProperty(propKey)) {
enqueuePutListener(this, propKey, nextProp, transaction);
}
}
}
}
//这里进行事件绑定
首先判断了 rootContainerElement是不是一个 document或者 Fragment(文档片段节点)
enqueuePutListener 这个方法只在浏览器环境下执行,传给listenTo参数分别是事件名称'onclick'和代理事件的绑
定dom。如果是fragement 就是根节点(在reactDom.render指定的),不是的话就是document。listenTo
用于绑定事件到 document ,下面交由事务处理的是回调函数的存储,便于调用。
ReactBrowserEventEmitter 文件中的 listenTo 看做事件处理的源头。
这里获取了当前组件(其实这时候就是button)所在的document
function enqueuePutListener(inst, registrationName, listener, transaction) {
...
var containerInfo = inst._hostContainerInfo;
var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
listenTo(registrationName, doc);
...
}
Точкой привязки здесь является метод listenTo. См. исходный код (ReactBrowerEventEmitter)
//registrationName:需要绑定的事件
//当前component所属的document,即事件需要绑定的位置
listenTo: function (registrationName, contentDocumentHandle) {
var mountAt = contentDocumentHandle;
//获取当前document上已经绑定的事件
var isListening = getListeningForDocument(mountAt);
// 获取 registrationName(注册事件名称)的topLevelEvent(顶级事件类型)
var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];
for (var i = 0; i < dependencies.length; i++) {
var dependency = dependencies[i];
if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
if (dependency === 'topWheel') {
...
} else if (dependency === 'topScroll') {
...
} else if (dependency === 'topFocus' || dependency === 'topBlur') {
...
} else if (topEventMapping.hasOwnProperty(dependency)) {
// 获取 topLevelEvent 对应的浏览器原生事件
//冒泡处理
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
}
isListening[dependency] = true;
}
}
},
Для одного и того же события, такого как щелчок, есть два события onClick (срабатывает на этапе всплытия) и onClickCapture (срабатывает на этапе захвата) с двумя именами событий. Это всплытие и захват имитируются событием реакции. События, привязанные к документу, в основном находятся на стадии всплытия (с дополнительной обработкой для whell, focus, scroll), как показано на рисунке ниже, привязка события клика выполняется следующим образом.
Последняя обработка (прослушивание и захват EventListener)
//eventType:事件类型,target: document对象,
//callback:是固定的,始终是ReactEventListener的dispatch方法
if (target.addEventListener) {
target.addEventListener(eventType, callback, false);
return {
remove: function remove() {
target.removeEventListener(eventType, callback, false);
}
};
}
Все события привязаны к документу
Таким образом, события запускаются методом отправки ReactEventListener.
Хранение обратных вызовов
Увидев это, вы можете быть сбиты с толку: все обратные вызовы выполняют метод диспетчеризации ReactEventListener, так почему же написанный мной обратный вызов сработал. Не волнуйтесь, читайте дальше:
function enqueuePutListener(inst, registrationName, listener, transaction) {
...
//注意这里!!!!!!!!!
//这里获取了当前组件(其实这时候就是button)所在的document
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
//事件绑定
listenTo(registrationName, doc);
//这段代码表示将putListener放入回调序列,当组件挂载完成是会依次执行序列中的回调。putListener也是在那时候执行的。
//不明白的可以看看本专栏中前两篇关于transaction和挂载机制的讲解
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener
});
//保存回调
function putListener() {
var listenerToPut = this;
EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
}
}
Тем не менее в этом коде мы ввели привязку событий, в основном метод listenTo. PutListener выполняется, когда привязка завершена. Этот метод будет выполняться на этапе закрытия транзакции ReactReconcileTransaction, которым управляет EventPluginHub.
//
var listenerBank = {};
var getDictionaryKey = function (inst) {
//inst为组建的实例化对象
//_rootNodeID为组件的唯一标识
return '.' + inst._rootNodeID;
}
var EventPluginHub = {
//inst为组建的实例化对象
//registrationName为事件名称
//listner为我们写的回调函数,也就是列子中的this.autoFocus
putListener: function (inst, registrationName, listener) {
...
var key = getDictionaryKey(inst);
var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
bankForRegistrationName[key] = listener;
...
}
}
EventPluginHub создается только один раз для каждого проекта. То есть обратные вызовы всех событий группы проекта будут храниться в уникальном банке прослушивателей.
Немного кружится голова, поставьте блок-схему и внимательно ее вспомните.
триггер события
При регистрации событий мы сказали, что все события привязаны к Документу. Унификация обратного вызова — это метод отправки ReactEventListener. Из-за всплывающего механизма, независимо от того, на какой DOM мы нажимаем, документ в конечном итоге ответит (поскольку другие DOM вообще не имеют прослушивателей событий). То есть отправка будет запущена
dispatchEvent: function(topLevelType, nativeEvent) {
//实际触发事件的DOM对象
var nativeEventTarget = getEventTarget(nativeEvent);
//nativeEventTarget对应的virtual DOM
var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(
nativeEventTarget,
);
...
//创建bookKeeping实例,为handleTopLevelImpl回调函数传递事件名和原生事件对象
//其实就是把三个参数封装成一个对象
var bookKeeping = getTopLevelCallbackBookKeeping(
topLevelType,
nativeEvent,
targetInst,
);
try {
//这里开启一个transactIon,perform中执行了
//handleTopLevelImpl(bookKeeping)
ReactGenericBatching.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
},
function handleTopLevelImpl(bookKeeping) {
//触发事件的真实DOM
var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
//nativeEventTarget对应的ReactElement
var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
//bookKeeping.ancestors保存的是组件。
var ancestor = targetInst;
do {
bookKeeping.ancestors.push(ancestor);
ancestor = ancestor && findParent(ancestor);
} while (ancestor);
for (var i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
//具体处理逻辑
ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
}
}
//这就是核心的处理了
handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
//首先封装event事件
var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
//发送包装好的event
runEventQueueInBatch(events);
}
инкапсуляция событий
Первый — это extractEvents EventPluginHub.
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
var events;
var plugins = EventPluginRegistry.plugins;
for (var i = 0; i < plugins.length; i++) {
// Not every plugin in the ordering may be loaded at runtime.
var possiblePlugin = plugins[i];
if (possiblePlugin) {
//主要看这边
var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
......
}
}
return events;
},
Затем посмотрите на метод SimpleEventPlugin
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
......
//这里是对事件的封装,但是不是我们关注的重点
var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
//重点看这边
EventPropagators.accumulateTwoPhaseDispatches(event);
return event;
}
Далее идут различные отсылки в методе, прыжок, прыжок, поворот, поворот, мы подошли к методу traverseTwoPhase в ReactDOMTraversal
//inst是触发事件的target的ReactElement
//fn:EventPropagator的accumulateDirectionalDispatches
//arg: 就是之前部分封装好的event(之所以说是部分,是因为现在也是在处理Event,这边处理完才是封装完成)
function traverseTwoPhase(inst, fn, arg) {
var path = [];
while (inst) {
//注意path,这里以ReactElement的形式冒泡着,
//把触发事件的父节点依次保存下来
path.push(inst);
//获取父节点
inst = inst._hostParent;
}
var i;
//捕捉,依次处理
for (i = path.length; i-- > 0;) {
fn(path[i], 'captured', arg);
}
//冒泡,依次处理
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
//判断父组件是否保存了这一类事件
function accumulateDirectionalDispatches(inst, phase, event) {
//获取到回调
var listener = listenerAtPhase(inst, event, phase);
if (listener) {
//如果有回调,就把包含该类型事件监听的DOM与对应的回调保存进Event。
//accumulateInto可以理解成_.assign
//记住这两个属性,很重要。
event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
В listenerAtPhase выполняется функция getListener EventPluginHub.
getListener: function (inst, registrationName) {
//还记得之前保存回调的listenerBank吧?
var bankForRegistrationName = listenerBank[registrationName];
if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
return null;
}
//获取inst的_rootNodeId
var key = getDictionaryKey(inst);
//获取对应的回调
return bankForRegistrationName && bankForRegistrationName[key];
},
распределение событий
runEventQueueInBatch в основном выполняет двухэтапные операции
function runEventQueueInBatch(events) {
//将event事件加入processEventQueue序列
EventPluginHub.enqueueEvents(events);
//前一步保存好的processEventQueue依次执行
//executeDispatchesAndRelease
EventPluginHub.processEventQueue(false);
}
processEventQueue: function (simulated) {
var processingEventQueue = eventQueue;
eventQueue = null;
if (simulated) {
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
} else {
//重点看这里
//forEachAccumulated可以看成forEach的封装
//那么这里就是processingEventQueue保存的event依次执行executeDispatchesAndReleaseTopLevel(event)
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
}
},
executeDispatchesAndReleaseTopLevel (событие) — это еще одна оболочка функции, и последнее, что нужно сделать, это
function executeDispatchesInOrder(event, simulated) {
//对应的回调函数数组
var dispatchListeners = event._dispatchListeners;
//有eventType属性的ReactElement数组
var dispatchInstances = event._dispatchInstances;
......
if (Array.isArray(dispatchListeners)) {
for (var i = 0; i < dispatchListeners.length; i++) {
if (event.isPropagationStopped()) {
break;
}
executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
}
} else if (dispatchListeners) {
executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
}
event._dispatchListeners = null;
event._dispatchInstances = null;
}
Хорошо, наконец-то здесь появился старый знакомый — при инкапсуляции nativeEvent здесь работают два атрибута, которые мы сохранили в событии, dispatchListeners и dispatchInstances. Код очень простой, если есть функция обратного вызова для обработки этого события, оно будет обработано один раз. Мы обсудим детали позже, давайте посмотрим, как это обрабатывается здесь.
function executeDispatch(event, simulated, listener, inst) {
//type是事件类型
var type = event.type || 'unknown-event';
//这是触发事件的真实DOM,也就是列子中的button
event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
if (simulated) {
ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
} else {
//看这里看这里
ReactErrorUtils.invokeGuardedCallback(type, listener, event);
}
event.currentTarget = null;
}
Наконец дошло до конца, код в ReactErrorUtil (Чтобы помочь в разработке, React лучше интегрируется с инструментами разработки, имитируя реальные события браузера. Этот код работает в режиме разработки)
//创造一个临时DOM
var fakeNode = document.createElement('react');
ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
//绑定回调函数的上下文
var boundFunc = func.bind(null, a);
//定义事件类型
var evtType = 'react-' + name;
//绑定事件
fakeNode.addEventListener(evtType, boundFunc, false);
//生成原生事件
var evt = document.createEvent('Event');
//将原生事件处理成我们需要的类型
evt.initEvent(evtType, false, false);
//发布事件---这里会执行回调
fakeNode.dispatchEvent(evt);
//移出事件监听
fakeNode.removeEventListener(evtType, boundFunc, false);
};