对React熟悉的同学都知道,React中的事件机制并不是原生的那一套,事件没有绑定在原生DOM上,发出的事件也是对原生事件的包装。
那么这一切是怎么实现的呢?

事件注册

首先还是看我们熟悉的代码


<button onClick={this.autoFocus}>点击聚焦</button>

这是我们在React中绑定事件的常规写法。经由JSX解析,button会被当做组件挂载。而onClick这时候也只是一个普通的props。
ReactDOMComponent在进行组件加载(mountComponent)、更新(updateComponent)的时候,需要对props进行处理(_updateDOMProperties):


ReactDOMComponent.Mixin = {
_updateDOMProperties: function (lastProps, nextProps, transaction) {
...
for (propKey in nextProps) {
// 判断是否为事件属性
if (registrationNameModules.hasOwnProperty(propKey)) {
enqueuePutListener(this, propKey, nextProp, transaction);
}
}
}
}
//这里进行事件绑定
function enqueuePutListener(inst, registrationName, listener, transaction) {
...
//注意这里!!!!!!!!!
//这里获取了当前组件(其实这时候就是button)所在的document
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
listenTo(registrationName, doc);
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener
});
function putListener() {
var listenerToPut = this;
EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
}
}

绑定的重点是这里的listenTo方法。看源码(ReactBrowerEventEmitter)


//registrationName:需要绑定的事件
//当前component所属的document,即事件需要绑定的位置
listenTo: function (registrationName, contentDocumentHandle) {
var mountAt = contentDocumentHandle;
//获取当前document上已经绑定的事件
var isListening = getListeningForDocument(mountAt);
...
if (...) {
//冒泡处理
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(...);
} else if (...) {
//捕捉处理
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(...);
}
...
},

最后处理(EventListener的listen和capture中)


//eventType:事件类型,target: document对象,
//callback:是固定的,始终是ReactEventListener的dispatch方法
if (target.addEventListener) {
target.addEventListener(eventType, callback, false);
return {
remove: function remove() {
target.removeEventListener(eventType, callback, false);
}
};
}

从事件注册的机制中不难看出:

  • 所有事件绑定在document上
  • 所以事件触发的都是ReactEventListener的dispatch方法

回调储存

看到这边你可能疑惑,所有回调都执行的ReactEventListener的dispatch方法,那我写的回调干嘛去了。别急,接着看:


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事务的close阶段执行,具体由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在每个项目中只实例化一次。也就是说,项目组所有事件的回调都会储存在唯一的listenerBank中。

是不是有点晕,放上流程图,仔细回忆一下

事件触发

注册事件时我们说过,所有的事件都是绑定在Document上。回调统一是ReactEventListener的dispatch方法。
由于冒泡机制,无论我们点击哪个DOM,最后都是由document响应(因为其他DOM根本没有事件监听)。也即是说都会触发dispatch


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);
}

事件封装

首先是EventPluginHubextractEvents


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;
}

接下来是方法中的各种引用,跳啊跳,转啊转,我们来到了ReactDOMTraversal中的traverseTwoPhase方法


//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里面执行的是EventPluginHub的getListener函数


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];
},

可以发现,React在分装原生nativeEvent时

  • 将有eventType属性的ReactElement放入 event._dispatchInstances
  • 将对应的回调依次放入event._dispatchListeners

事件分发

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(event)又是各种函数包装,最后干活的是


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;
}

OK,这里总算出现了老熟人,在封装nativeEvent时我们保存在event里的两个属性,dispatchListenersdispatchInstances,在这里起作用。
代码很简单,如果有处理这个事件的回调函数,就一次进行处理。细节我们稍后讨论,先看看这里是怎么处理的吧


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通过模拟真正的浏览器事件来获得更好的devtools集成。这段代码在开发模式下运行)


//创造一个临时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);
};

总体流程

不难发现,我们经历了从真实DOM到Virtual DOM的来回转化。

常见问题的答案。

  1. e.stopPropagation不能阻止原生事件冒泡
    event是封装好的事件。他是在document的回调里进行封装,并执行回调的。而原生的监听,在document接收到冒泡时早就执行完了。
  2. e.nativeEvent.stopPropagation,回调无法执行。
    很简单,因为冒泡是从里到外,执行了原生的阻止冒泡,document当如捕捉不到,document都没捕捉到,React还玩个球啊,要知道,一切操作都放在docuemnt的回调里了。
  3. 怎么避免两者影响


    这个答案大家说了很多次,避免原生事件与React事件混用,或者通过target进行判断。

为什么这么设计

在网上看过一个列子说得很好,
一个Ul下面有1000个li标签。想在想为每个li都绑定一个事件,怎么操作?
总不可能一个个绑定吧?
其实这个和jquery绑定事件差不多。通过最外层绑定事件,当操作是点击任何一个li自然会冒泡到最外面的Ul,又可以通过最外面的target获取到具体操作的DOM。一次绑定,收益一群啊。

来源:https://segmentfault.com/a/1190000011413181

源码看React 事件机制的更多相关文章

  1. 详解 QT 源码之 Qt 事件机制原理

    QT 源码之 Qt 事件机制原理是本文要介绍的内容,在用Qt写Gui程序的时候,在main函数里面最后依据都是app.exec();很多书上对这句的解释是,使 Qt 程序进入消息循环.下面我们就到ex ...

  2. 【React】354- 一文吃透 React 事件机制原理

    大纲 主要分为4大块儿,主要是结合源码对 react事件机制的原理 进行分析,希望可以让你对 react事件机制有更清晰的认识和理解. 当然肯定会存在一些表述不清或者理解不够标准的地方,还请各位大神. ...

  3. 从Chrome源码看浏览器的事件机制

    .aligncenter { clear: both; display: block; margin-left: auto; margin-right: auto } .crayon-line spa ...

  4. 从Chrome源码看JS Array的实现

    .aligncenter { clear: both; display: block; margin-left: auto; margin-right: auto } .crayon-line spa ...

  5. 从微信小程序开发者工具源码看实现原理(一)- - 小程序架构设计

    使用微信小程序开发已经很长时间了,对小程序开发已经相当熟练了:但是作为一名对技术有追求的前端开发,仅仅熟练掌握小程序的开发感觉还是不够的,我们应该更进一步的去理解其背后实现的原理以及对应的考量,这可能 ...

  6. 从微信小程序开发者工具源码看实现原理(四)- - 自适应布局

    从前面从微信小程序开发者工具源码看实现原理(一)- - 小程序架构设计可以知道,小程序大部分是通过web技术进行渲染的,也就是最终通过浏览器的dom tree + cssom来生成渲染树:既然最终是通 ...

  7. 从微信小程序开发者工具源码看实现原理(三)- - 双线程通信

    文章概览: 引言 小程序开发者工具双线程通信的设计 1.on: 用来收集小程序开发者工具触发的事件回调 2.invoke:以api方式调用开发工具提供的基础能力 3.publish:用来向Appser ...

  8. 从linux源码看epoll

    从linux源码看epoll 前言 在linux的高性能网络编程中,绕不开的就是epoll.和select.poll等系统调用相比,epoll在需要监视大量文件描述符并且其中只有少数活跃的时候,表现出 ...

  9. 从Linux源码看Socket(TCP)Client端的Connect

    从Linux源码看Socket(TCP)Client端的Connect 前言 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 今天笔者就来从Linux源码的 ...

随机推荐

  1. Codeforces 988D Points and Powers of Two ( 思维 || 二的幂特点 )

    题目链接 题意 : 给出坐标轴上的 n 个点的横坐标,要你选出最多的点,使得这些点两两距离是二的幂 ( 特殊情况 : 选出的集合只有一个点也满足条件 ) 分析 : 官方题解已经说的很好了 最关键是是判 ...

  2. POJ 2528 Mayor’s posters (线段树段替换 && 离散化)

    题意 : 在墙上贴海报, n(n<=10000)个人依次贴海报,给出每张海报所贴的范围li,ri(1<=li<=ri<=10000000).求出最后还能看见多少张海报. 分析 ...

  3. #333 Div2 Problem B Approximating a Constant Range (尺取 && RMQ || 尺取 && multiset)

    题目链接:http://codeforces.com/contest/602/problem/B 题意 :给出一个含有 n 个数的区间,要求找出一个最大的连续子区间使得这个子区间的最大值和最小值的差值 ...

  4. 游标定位:Cursor类

    关于 Cursor Cursor 是每行的集合. 使用 moveToFirst() 定位第一行. 你必须知道每一列的名称. 你必须知道每一列的数据类型. Cursor 是一个随机的数据源. 所有的数据 ...

  5. 2018百度之星初赛A轮 度度熊学队列

    注意:刚开始用数组存deque<int> qa[MAX]会爆内存 需要改用map<int, deque<int> > qa优化 不明觉厉 #include<b ...

  6. opengl中相关的计算机图形变换矩阵之:齐次坐标 (摘编)

    模型视图变换(几何变换)矩阵: 1. 齐次坐标:两条平行线也可以相交. 在欧几里得空间中,两条平行线是无法相交的,但是在投影空间(Projective Space)这条定理就不再适用了. 比如上图中, ...

  7. Unity3D 中的FOV

    一直以为Unity中的相机FOV指的是frustum两个对角边的方向夹角,所以在看一篇教程的时候怎么算都算不对.后来灵机一动,查了一下,才发现Unity中的Fov指的是垂直方向的FOV: 参见这里:h ...

  8. 3D Computer Grapihcs Using OpenGL - 18 相机移动

    移动相机需要用到键盘按键,按键事件的引入需要包含头文件 #include <Qt3DInput\qkeyevent.h> 并实现QWidget中定义的虚函数keyPressEvent 我们 ...

  9. @清晰掉 string.h之基础堵漏

    一个标准的strcpy函数: 原本以为自己对strcpy还算比较了解,结果面试时还是悲剧了. 下面给出网上strcpy的得分版本: 2分 void strcpy( char *strDest, cha ...

  10. 48 条高效率的 PHP 优化写法

    来源:歪麦博客 https://www.awaimai.com/1050.html 1 字符串 1.1 少用正则表达式 能用PHP内部字符串操作函数的情况下,尽量用他们,不要用正则表达式, 因为其效率 ...