React笔记-事件分发
事件分发
之前讲述了事件如何绑定在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);
};
topLevelType
为click
,nativeEvent
为真实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 work
。flushImmediateWork
里的调用关系很复杂,最终会调用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
中调用了两个方法: extractEvents
和runEventsInBatch
。前者构造合成事件,后者批处理合成事件。
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
事件合适的plugin
是SimpleEventPlugin
,其他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
属性即可获取。这里SyntheticMouseEvent
由SyntheticUIEvent
扩展而来,而SyntheticUIEvent
由SyntheticEvent
扩展而来。
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
合并到事件队列,之前没有处理完毕的队列也一同合并。如果新的事件队列为空,则退出。反之开始循环处理事件队列中每一个event
。forEachAccumulated
前面有提到过,这里不再赘述。
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
,会进行reconciliation
和commit
。由于事件流的执行是批处理过程,同步调用this.setState
不会立马更新,需等待所有事件执行完成,即scheduler
调度完后才开始performSyncWork
,最终才能拿到新的state
。如果是setTimeout
或者是在dom上另外addEventListener
的回调函数中调用this.setState
则会立马更新。因为执行回调函数的时候不经过React
事件流。
更好的阅读体验在我的github,欢迎
React笔记-事件分发的更多相关文章
- React笔记-事件注册
事件机制 本系列以React v16.8.3为基础进行源码分析 React事件主要分为两部分: 事件注册与事件分发.下面先从事件注册说起. 事件注册 假设我们的程序如下: <!DOCTYPE h ...
- Cocos2d-x 3.2 学习笔记(九)EventDispatcher事件分发机制
EventDispatcher事件分发机制先创建事件,注册到事件管理中心_eventDispatcher,通过发布事件得到响应进行回调,完成事件流. 有五种不同的事件机制:EventListenerT ...
- 自定义控件(视图)2期笔记10:自定义视图之View事件分发机制("瀑布流"的案例)
1. Touch事件的传递: 图解Touch事件的传递,如下: 当我们点击子View 02内部的Button控件时候,我们就触发了Touch事件. • 这个Touch事件首先传递给了顶级父View ...
- cocos2d-x-3.1 事件分发机制 (coco2d-x 学习笔记七)
触摸事件 Sprite* sp1 = Sprite::create("Images/t1.png"); sp1->setPosition(Vec2(visibleSize.w ...
- 自定义控件(视图)2期笔记14:自定义视图之View事件分发 dispatchTouchEvent,onTouch,onTouchEvent,onClick逻辑顺序过程
1. 这里我们先从案例角度说明dispatchTouchEvent,onTouch,onTouchEvent,onClick逻辑顺序过程: (1)首先我们重写一个MyButton 继承自 Button ...
- Android中View的事件分发机制——Android开发艺术探索笔记
原文链接 http://sparkyuan.me/ 转载请注明出处 介绍 点击事件的事件分发就是对MotionEvent事件的分发过程.当一个MotionEvent产生了以后,系统须要把这个事件传递给 ...
- Cocos2d-x 学习笔记(15.2) EventDispatcher 事件分发机制 dispatchEvent(event)
1. 事件分发方法 EventDispatcher::dispatchEvent(Event* event) 首先通过_isEnabled标志判断事件分发是否启用. 执行 updateDirtyFla ...
- Cocos2d-x 学习笔记(15.4) EventDispatcher 事件分发具体逻辑 dispatchEventToListeners函数
dispatchEvent(Event* event)方法在对事件对应的监听器进行重新排序后,进行事件分发操作.具体操作由dispatchEventToListeners方法执行. 该方法声明: vo ...
- Android程序员事件分发机制学习笔记
通过问题来学习一个东西是很好的方法.学习Android中View的事件体系,我也通过给自己提问题,在解决问题的同时也就知道了其中原理. 首先来几个问题起步: 什么是事件?什么是事件分发机制? 在我们通 ...
随机推荐
- VS网站开发的发布部署的不同情况说明
VS网站开发有两种模式: 1.网站模式 2.应用模式 其中,网站模式的发布,要考虑勾选“使用固定命名和单页程序集” 如下图 网站模式: 新建网站的网站模式 新建网站的网站模式第二步 应 ...
- Mysql引擎innodb_pool的作用
innodb_buffer_pool的简介: InnoDB主索引是聚簇索引,索引与数据共用表空间,对于InnoDB而言,数据就是索引,索引就是数据.InnoDB缓存机制和MyISAM缓存机制的最大区别 ...
- Mysql 5.7源码编译启动 报error问题:The server quit without updating PID file (/data/data_mysql/mysql.pid).
一般是报error问题就是我们的mysql没有权限,这里主要是指三点:一个是mysql的安装主目录要设为mysql用户和用户组.一个是logs目录设置为mysql用户以及用户组.还有一个是data目录 ...
- MySQL基础之---mysqlimport工具和LOAD DATA命令导入文本文件
1.mysqlimport工具的使用 看一下命令的使用方法: shell > mysqlimport -u root -p [--LOCAL] DBname File [option] --f ...
- 高斯消去、追赶法 matlab
1. 分别用Gauss消去法.列主元Gauss消去法.三角分解方法求解方程组 程序: (1)Guess消去法: function x=GaussXQByOrder(A,b) %Gauss消去法 N = ...
- 4.Dubbo2.5.3集群容错和负载均衡
转载请出自出处:http://www.cnblogs.com/hd3013779515/ 1.集群容错和负载均衡原理 各节点关系: 这里的Invoker是Provider的一个可调用Service的抽 ...
- Redis系列六:redis相关功能
一. 慢查询原因分析 与mysql一样:当执行时间超过阀值,会将发生时间耗时的命令记录 redis命令生命周期:发送 排队 执行 返回慢查询只统计第3个执行步骤的时间 预设阀值:两种方式,默认为10毫 ...
- BZOJ2744:[HEOI2012]朋友圈(最大团,乱搞)
Description 在很久很久以前,曾经有两个国家和睦相处,无忧无虑的生活着.一年一度的评比大会开始了,作为和平的两国,一个朋友圈数量最多的永远都是最值得他人的尊敬,所以现在就是需要你求朋友圈的最 ...
- vagrant设置虚拟机的名字
如果我们不在vagrant init 命令生成的vagrantfile文件中声明虚拟机的名字的话,一般会默认给我们指定一个名字,指定的方法: config.vm.provider "virt ...
- PAT B1013 数素数 (20 分)
令 Pi 表示第 i 个素数.现任给两个正整数 M≤N≤104,请输出 PM 到 PN 的所有素数. 输入格式: 输入在一行中给出 M 和 N,其间以空格分隔. 输出格式: 输 ...