事件分发

之前讲述了事件如何绑定在document上,那么具体事件触发的时候是如何分发到具体的监听者呢?我们接着上次注册的事件代理看。当我点击update counter按钮时,触发注册的click事件代理。

function dispatchInteractiveEvent(topLevelType, nativeEvent) {
interactiveUpdates(dispatchEvent, topLevelType, nativeEvent);
}
function interactiveUpdates(fn, a, b) {
return _interactiveUpdatesImpl(fn, a, b);
}
var _interactiveUpdatesImpl = function (fn, a, b) {
return fn(a, b);
};

topLevelTypeclicknativeEvent为真实dom事件对象。看似很多,其实就做了一件事: 执行dispatchEvent(topLevelType, nativeEvent)。其实不然,_interactiveUpdatesImpl在后面被重新赋值为interactiveUpdates$1,完成了一次自我蜕变。

function setBatchingImplementation(batchedUpdatesImpl, interactiveUpdatesImpl, flushInteractiveUpdatesImpl) {
_batchedUpdatesImpl = batchedUpdatesImpl;
_interactiveUpdatesImpl = interactiveUpdatesImpl;
_flushInteractiveUpdatesImpl = flushInteractiveUpdatesImpl;
} function interactiveUpdates$1(fn, a, b) {
if (!isBatchingUpdates && !isRendering && lowestPriorityPendingInteractiveExpirationTime !== NoWork) {
performWork(lowestPriorityPendingInteractiveExpirationTime, false);
lowestPriorityPendingInteractiveExpirationTime = NoWork;
}
var previousIsBatchingUpdates = isBatchingUpdates;
isBatchingUpdates = true;
try {
return scheduler.unstable_runWithPriority(scheduler.unstable_UserBlockingPriority, function () {
return fn(a, b);
});
} finally {
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
performSyncWork();
}
}
} setBatchingImplementation(batchedUpdates$1, interactiveUpdates$1, flushInteractiveUpdates$1);

如果有任何等待的交互更新,条件满足的情况下会先同步更新,然后设置isBatchingUpdates,进行scheduler调度。最后同步更新。scheduler的各类优先级如下:

unstable_ImmediatePriority: 1
unstable_UserBlockingPriority: 2
unstable_NormalPriority: 3
unstable_LowPriority: 4
unstable_IdlePriority: 5

进入scheduler调度,根据优先级计算时间,开始执行传入的回调函数。然后调用dispatchEvent,最后更新immediate workflushImmediateWork里的调用关系很复杂,最终会调用requestAnimationFrame进行更新,这里不进行过多讨论。

function unstable_runWithPriority(priorityLevel, eventHandler) {
switch (priorityLevel) {
case ImmediatePriority:
case UserBlockingPriority:
case NormalPriority:
case LowPriority:
case IdlePriority:
break;
default:
priorityLevel = NormalPriority;
} var previousPriorityLevel = currentPriorityLevel;
var previousEventStartTime = currentEventStartTime;
currentPriorityLevel = priorityLevel;
currentEventStartTime = exports.unstable_now(); try {
return eventHandler();
} finally {
currentPriorityLevel = previousPriorityLevel;
currentEventStartTime = previousEventStartTime;
flushImmediateWork();
}
}

下面看看dispatchEvent的具体执行过程。

function dispatchEvent(topLevelType, nativeEvent) {
if (!_enabled) {
return;
}
// 获取事件触发的原始节点
var nativeEventTarget = getEventTarget(nativeEvent);
// 获取原始节点最近的fiber对象(通过缓存在dom上的internalInstanceKey属性来寻找),如果没找到会往父节点继续寻找。
var targetInst = getClosestInstanceFromNode(nativeEventTarget); if (targetInst !== null && typeof targetInst.tag === 'number' && !isFiberMounted(targetInst)) {
targetInst = null;
}
// 创建对象,包含事件名称,原始事件,目标fiber对象和ancestor(空数组);如果缓存池有则直接取出并根据参数初始化属性。
var bookKeeping = getTopLevelCallbackBookKeeping(topLevelType, nativeEvent, targetInst); try {
// 批处理事件
batchedUpdates(handleTopLevel, bookKeeping);
} finally {
// 释放bookKeeping对象内存,并放入对象池缓存
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
}

接着看batchedUpdates,其实就是设置isBatching变量然后调用handleTopLevel(bookkeeping)

function batchedUpdates(fn, bookkeeping) {
if (isBatching) {
return fn(bookkeeping);
}
isBatching = true;
try {
// _batchedUpdatesImpl其实指向batchedUpdates$1函数,具体细节这里不再赘述
return _batchedUpdatesImpl(fn, bookkeeping);
} finally {
isBatching = false;
var controlledComponentsHavePendingUpdates = needsStateRestore();
if (controlledComponentsHavePendingUpdates) {
_flushInteractiveUpdatesImpl();
restoreStateIfNeeded();
}
}
}

所以将原始节点对应最近的fiber缓存在bookKeeping.ancestors中。

function handleTopLevel(bookKeeping) {
var targetInst = bookKeeping.targetInst;
var ancestor = targetInst;
do {
if (!ancestor) {
bookKeeping.ancestors.push(ancestor);
break;
}
var root = findRootContainerNode(ancestor);
if (!root) {
break;
}
bookKeeping.ancestors.push(ancestor);
ancestor = getClosestInstanceFromNode(root);
} while (ancestor); for (var i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
runExtractedEventsInBatch(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
}
}

runExtractedEventsInBatch中调用了两个方法: extractEventsrunEventsInBatch。前者构造合成事件,后者批处理合成事件。

function runExtractedEventsInBatch(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
var events = extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
runEventsInBatch(events);
}

事件合成

 function extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
var events = null; for (var i = 0; i < plugins.length; i++) {
var possiblePlugin = plugins[i];
if (possiblePlugin) {
var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget); if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
}
}
} return events;
}

plugins是所有合成事件集合的数组,EventPluginHub初始化的时候完成注入。遍历所有plugins,调用其extractEvents方法,返回构造的合成事件。accumulateInto函数则把合成事件放入events。本例click事件合适的pluginSimpleEventPlugin,其他plugin得到的extractedEvents都不满足if (extractedEvents)条件。

EventPluginHubInjection.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin,
});

接下来看看构造合成事件的具体过程,这里针对SimpleEventPlugin,其他plugin就不一一分析了,来看下其extractEvents:

extractEvents: function(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
if (!dispatchConfig) {
return null;
}
var EventConstructor = void 0;
switch (topLevelType) {
...
case TOP_CLICK:
...
EventConstructor = SyntheticMouseEvent;
break;
...
}
var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
accumulateTwoPhaseDispatches(event);
return event;
}

topLevelEventsToDispatchConfig是一个map对象,存储着各类事件对应的配置信息。这里获取到click的配置信息,然后根据topLevelType选择对应的合成构造函数,这里为SyntheticMouseEvent。接着从SyntheticMouseEvent合成事件对象池中获取合成事件。调用EventConstructor.getPooled,最终调用的是getPooledEvent

注意: SyntheticEvent.extend方法中明确写有addEventPoolingTo(Class);所以,SyntheticMouseEvent有eventPool、getPooled和release属性。后面会详细介绍SyntheticEvent.extend

function addEventPoolingTo(EventConstructor) {
EventConstructor.eventPool = [];
EventConstructor.getPooled = getPooledEvent;
EventConstructor.release = releasePooledEvent;
}

首次触发事件,对象池为空,所以这里需要新创建。如果不为空,则取出一个并初始化。

function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
var EventConstructor = this;
if (EventConstructor.eventPool.length) {
var instance = EventConstructor.eventPool.pop();
EventConstructor.call(instance, dispatchConfig, targetInst, nativeEvent, nativeInst);
return instance;
}
return new EventConstructor(dispatchConfig, targetInst, nativeEvent, nativeInst);
}

合成事件的属性是由React主动生成的,一些属性和原生事件的属性名完全一致,使其完全符合W3C标准,因此在事件层面上具有跨浏览器兼容性。如果要访问原生对象,通过nativeEvent属性即可获取。这里SyntheticMouseEventSyntheticUIEvent扩展而来,而SyntheticUIEventSyntheticEvent扩展而来。

var SyntheticMouseEvent = SyntheticUIEvent.extend({
...
}); var SyntheticUIEvent = SyntheticEvent.extend({
...
}); SyntheticEvent.extend = function (Interface) {
var Super = this;
// 原型继承
var E = function () {};
E.prototype = Super.prototype;
var prototype = new E();
// 构造继承
function Class() {
return Super.apply(this, arguments);
}
_assign(prototype, Class.prototype);
Class.prototype = prototype;
Class.prototype.constructor = Class; Class.Interface = _assign({}, Super.Interface, Interface);
Class.extend = Super.extend;
addEventPoolingTo(Class); return Class;
};

当被new创建时,会调用父类SyntheticEvent进行构造。主要是将原生事件上的属性挂载到合成事件上,还配置了一些额外属性。

function SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) {
this.dispatchConfig = dispatchConfig;
this._targetInst = targetInst;
this.nativeEvent = nativeEvent;
...
}

合成事件构造完成后,调用accumulateTwoPhaseDispatches

function accumulateTwoPhaseDispatches(events) {
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
} // 循环处理所有的合成事件
function forEachAccumulated(arr, cb, scope) {
if (Array.isArray(arr)) {
arr.forEach(cb, scope);
} else if (arr) {
cb.call(scope, arr);
}
} // 检测事件是否具有捕获阶段和冒泡阶段
function accumulateTwoPhaseDispatchesSingle(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
}
} function traverseTwoPhase(inst, fn, arg) {
var path = [];
// 循环遍历当前元素及父元素,缓存至path
while (inst) {
path.push(inst);
inst = getParent(inst);
}
var i = void 0;
// 捕获阶段
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);
// 将相关listener和目标fiber挂载到event对应的属性上
if (listener) {
event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}

事件执行(批处理合成事件)

首先将events合并到事件队列,之前没有处理完毕的队列也一同合并。如果新的事件队列为空,则退出。反之开始循环处理事件队列中每一个eventforEachAccumulated前面有提到过,这里不再赘述。

function runEventsInBatch(events) {
if (events !== null) {
eventQueue = accumulateInto(eventQueue, events);
}
var processingEventQueue = eventQueue;
eventQueue = null; if (!processingEventQueue) {
return;
} forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
rethrowCaughtError();
}

接下来看看事件处理,executeDispatchesAndRelease方法将事件执行和事件清理分开。

var executeDispatchesAndReleaseTopLevel = function (e) {
return executeDispatchesAndRelease(e);
}; var executeDispatchesAndRelease = function (event) {
if (event) {
// 执行事件
executeDispatchesInOrder(event); if (!event.isPersistent()) {
// 事件清理,将合成事件放入对象池
event.constructor.release(event);
}
}
};

提取事件的处理函数和对应的fiber,调用executeDispatch

function executeDispatchesInOrder(event) {
var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._dispatchInstances;
if (Array.isArray(dispatchListeners)) {
for (var i = 0; i < dispatchListeners.length; i++) {
if (event.isPropagationStopped()) {
break;
}
executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
}
} else if (dispatchListeners) {
executeDispatch(event, dispatchListeners, dispatchInstances);
}
event._dispatchListeners = null;
event._dispatchInstances = null;
}

获取真实dom挂载到event对象上,然后开始执行事件。

function executeDispatch(event, listener, inst) {
var type = event.type || 'unknown-event';
// 获取真实dom
event.currentTarget = getNodeFromInstance(inst);
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
event.currentTarget = null;
}

invokeGuardedCallbackAndCatchFirstError下面调用的方法很多,最终会来到invokeGuardedCallbackImpl,关键就在func.apply(context, funcArgs);这里的func就是listener(本例中是handleClick),而funcArgs就是合成事件对象。至此,事件执行完毕。

var invokeGuardedCallbackImpl = function (name, func, context, a, b, c, d, e, f) {
var funcArgs = Array.prototype.slice.call(arguments, 3);
try {
func.apply(context, funcArgs);
} catch (error) {
this.onError(error);
}
};

事件清理

事件执行完之后,剩下就是一些清理操作。event.constructor.release(event)相当于releasePooledEvent(event)。由于click对应的是SyntheticMouseEvent,所以会放入SyntheticMouseEvent.eventPool中。EVENT_POOL_SIZE固定为10。

function releasePooledEvent(event) {
var EventConstructor = this;
event.destructor();
if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
EventConstructor.eventPool.push(event);
}
}

这里做了两件事,第一手动释放event属性上的内存(将属性置为null),第二将event放入对象池。至此,清理工作完毕。

destructor: function () {
...
this.dispatchConfig = null;
this._targetInst = null;
this.nativeEvent = null;
this.isDefaultPrevented = functionThatReturnsFalse;
this.isPropagationStopped = functionThatReturnsFalse;
this._dispatchListeners = null;
this._dispatchInstances = null;
...
}

event清理完后,还会清理bookKeeping,同样也会放入对象池进行缓存。同样CALLBACK_BOOKKEEPING_POOL_SIZE也固定为10。

// callbackBookkeepingPool是react-dom中的全局变量
function releaseTopLevelCallbackBookKeeping(instance) {
instance.topLevelType = null;
instance.nativeEvent = null;
instance.targetInst = null;
instance.ancestors.length = 0; if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) {
callbackBookkeepingPool.push(instance);
}
}

总结

最后执行performSyncWork。如果执行的事件内调用了this.setState,会进行reconciliationcommit。由于事件流的执行是批处理过程,同步调用this.setState不会立马更新,需等待所有事件执行完成,即scheduler调度完后才开始performSyncWork,最终才能拿到新的state。如果是setTimeout或者是在dom上另外addEventListener的回调函数中调用this.setState则会立马更新。因为执行回调函数的时候不经过React事件流。

更好的阅读体验在我的github,欢迎

React笔记-事件分发的更多相关文章

  1. React笔记-事件注册

    事件机制 本系列以React v16.8.3为基础进行源码分析 React事件主要分为两部分: 事件注册与事件分发.下面先从事件注册说起. 事件注册 假设我们的程序如下: <!DOCTYPE h ...

  2. Cocos2d-x 3.2 学习笔记(九)EventDispatcher事件分发机制

    EventDispatcher事件分发机制先创建事件,注册到事件管理中心_eventDispatcher,通过发布事件得到响应进行回调,完成事件流. 有五种不同的事件机制:EventListenerT ...

  3. 自定义控件(视图)2期笔记10:自定义视图之View事件分发机制("瀑布流"的案例)

    1. Touch事件的传递:   图解Touch事件的传递,如下: 当我们点击子View 02内部的Button控件时候,我们就触发了Touch事件. • 这个Touch事件首先传递给了顶级父View ...

  4. cocos2d-x-3.1 事件分发机制 (coco2d-x 学习笔记七)

    触摸事件 Sprite* sp1 = Sprite::create("Images/t1.png"); sp1->setPosition(Vec2(visibleSize.w ...

  5. 自定义控件(视图)2期笔记14:自定义视图之View事件分发 dispatchTouchEvent,onTouch,onTouchEvent,onClick逻辑顺序过程

    1. 这里我们先从案例角度说明dispatchTouchEvent,onTouch,onTouchEvent,onClick逻辑顺序过程: (1)首先我们重写一个MyButton 继承自 Button ...

  6. Android中View的事件分发机制——Android开发艺术探索笔记

    原文链接 http://sparkyuan.me/ 转载请注明出处 介绍 点击事件的事件分发就是对MotionEvent事件的分发过程.当一个MotionEvent产生了以后,系统须要把这个事件传递给 ...

  7. Cocos2d-x 学习笔记(15.2) EventDispatcher 事件分发机制 dispatchEvent(event)

    1. 事件分发方法 EventDispatcher::dispatchEvent(Event* event) 首先通过_isEnabled标志判断事件分发是否启用. 执行 updateDirtyFla ...

  8. ‎Cocos2d-x 学习笔记(15.4) EventDispatcher 事件分发具体逻辑 dispatchEventToListeners函数

    dispatchEvent(Event* event)方法在对事件对应的监听器进行重新排序后,进行事件分发操作.具体操作由dispatchEventToListeners方法执行. 该方法声明: vo ...

  9. Android程序员事件分发机制学习笔记

    通过问题来学习一个东西是很好的方法.学习Android中View的事件体系,我也通过给自己提问题,在解决问题的同时也就知道了其中原理. 首先来几个问题起步: 什么是事件?什么是事件分发机制? 在我们通 ...

随机推荐

  1. 使用AndroidStudio上传忽略文件至SVN Server的解决措施

    在同组项目进行共享时,容易把本地的配置文件比如*.iml等文件上传至共享服务器,这样会对队友造成巨大的麻烦,为了解决这个问题,可以使用下面方法解决,下面以上传到服务器的app.iml文件为例. 一.在 ...

  2. 使用TuShare下载历史逐笔成交数据并生成1分钟线

    使用如下代码从TuShare下载沪深300每只股票的历史成交记录并按股票.日期保存到本地.主要是为了以后查询方便快速. #-*- coding: utf-8 -*- import numpy as n ...

  3. Alpha冲刺报告(5/12)(麻瓜制造者)

    今日已完成 明日计划 部分api示意图 燃尽图 scrum会议照片 今日已完成 邓弘立: 完成部分首页逻辑功能 符天愉: 写代码写着写着想起来昨天的登录接口有个非常zz的逻辑错误,今天修改完后应该没有 ...

  4. mac brew安装mysql

    mac不自带mysql,这里需要重新安装,方法依然很简单 brew install mysql unset TMPDIR mysql_install_db --verbose --user=`whoa ...

  5. 2.python数据结构的性能分析

    一.引言 - 现在大家对 大O 算法和不同函数之间的差异有了了解.本节的目标是告诉你 Python 列表和字典操作的 大O 性能.然后我们将做一些基于时间的实验来说明每个数据结构的花销和使用这些数据结 ...

  6. 分享-结合demo讲解JS引擎工作原理

    代码如下: var x = 1; function A(y){ var x = 2; function B(z){ console.log(x+y+z); } return B; } var C = ...

  7. 利用 share code 插件同步代码片段

    利用 Settings Sync可以同步 VS code 配置,但它只能同步插件,利用  Settings Sync 再配合 share code 插件可以同步自定义代码片段,可以把 VS code ...

  8. Posts Tagged ‘This system is not registered to Red Hat Subscription Management. You can use subscription-manager to register问题的解决办法

    HowTo Install redhat package with YUM command without RHN February 26, 2014 in Redhat / Linux Tips a ...

  9. JAVA 第三周学习总结

    20175308 2018-2019-2 <Java程序设计>第三周学习总结 教材学习内容总结 本周的学习内容为整个第四章的内容,学习中感觉知识点既多又杂,故在总结时尽量选用重要的或高度概 ...

  10. Python:基础知识

    python是一种解释型.面向对象的.带有动态语义的高级程序语言. 一.下载安装 官网下载地址:https://www.python.org/downloads 下载后执行安装文件,按照默认安装顺序安 ...