先回顾一下 React 事件机制基本理解,React 自身实现了一套自己的事件机制,包括事件注册、事件合成、事件冒泡、事件派发等,虽然和原生是两码事,但是也是基于浏览器的事件机制下完成的。
我们都知道 React 的所有事件并没有绑定到具体的 DOM 节点,而是绑定到 document 上,然后由统一的事件处理程序来处理,同时也是基于浏览器的事件机制(冒泡),所有节点的时间都会在 document 上触发。
如果一个节点同时绑定了合成和原生事件,那么禁止冒泡后执行关系是怎样?
因为合成事件的触发是基于浏览器的事件机制来实现的,通过冒泡机制冒泡到最顶层元素,然后再由 dispatchEvent 统一去处理。
得出结论:
原生事件阻止冒泡肯定会组织合成事件的触发,合成事件的阻止冒泡不会影响原生事件。
原因在于,浏览器的事件执行机制是执行在前,冒泡在后,所以在原生事件中阻止冒泡会阻止合成事件的执行,反之不成立。
综上,两者最好不要一起使用,避免出现一些奇怪的问题。
React 将事件全部统一交给 document 来委托处理的原因是:
既然我们对 React 的事件机制有了初步的了解,那么可以知道合成事件并不是简单的合成和处理,从广义上还包括:
上面的代码是个一个元素添加点击事件的回调函数,方法中的参数 e 其实并不是原生事件中的 event,而是 React 包装过的对象,同时原生事件中的 event 被放在了这个对象的 nativeEvent 字段。
img再看下官网文档
imgSyntheticEvent 是 React 合成事件的基类,定义了合成事件的基础公共属性和方法。
React 会根据当前的事件类型来使用不同的合成事件对象,比如鼠标:点击事件 -- SyntheticMouseEvent,焦点事件 -- SyntheticFocusEvent 等,但都是继承与 SyntheticEvent。
img img img对于有些 DOM 元素事件,我们进行事件绑定之后,Reacgt 并不是只处理你生命的事件类型,还会额外增加一些其他的事件,帮助我们提升交互和体验。
比如说:
当我们给 input 生命一个 onChange 事件,React 帮我们做了很多工作:
img可以看到 React 不只是帮助我们注册一个 onchange 事件,还注册了很多其他的事件。
而这时候我们向文本框输入内容的时候,是可以实时得到内容,
然而原生事件只注册了一个 onchange 的话,需要在失去焦点的时候才能触发这个事件,这个缺陷 React 帮我们弥补了。
ps:图中有一个 invalid 事件是注册在当前元素而非在 document 的,可能是因为这个事件是 HTML5 表单属性特有的,需要在输入框输入的时候进行校验,如果是放到 document 上就不会生效了。
react 在给 document 注册事件的时候也是做了兼容性处理的。
img上面这个代码就可以看出,在给 document 注册事件的时候,内部也同时对 IE 浏览器做了兼容处理。
React 事件注册其实主要做了两件事情:
首先 React 拿到将要挂载在组件的虚拟 DOM(React Element 对象),然后处理 React DOM 的 props,判断属性内是否有声明为事件的属性,比如 onClick、onChange 等,这个时候得到事件类型 click、change 等和与之对应的回调函数,然后执行后面三步:
//...省略
经过 babel 编译之后,我们看到最终调用方法是 React.createElement
,而且生命的事件类型和回调函数就是个 props
React.createElement
执行的结果会返回一个所谓的虚拟DOM(React Element Object)。
ReactDOMComponent 在进行组件加载(mount)、更新(update)的时候,需要对 props 进行处理(_updateDOMProperties):
img可以看下 registrationNameModules 里面的内容,就是一个内置的常量:
img接着上面的代码执行到了这个方法
declare
在这个方法会进行事件的注册以及事件的存储,包括冒泡和捕获的处理
img根据当前的组件实例获取到最高父级,也就是 document,然后执行方法 listenTo,也是另一个很关键的方法进行事件绑定处理。
img最后执行 EventListener.listen
(冒泡)或者 EventListener.capture
(捕获),但看下冒泡的注册,其实就是 addEventListener
第三个参数设置为 false。
同时我们看到这里也同样对 IE 浏览器做了兼容。
上面没有看到 dispatchEvent 的定义,下面可以看到传入 dispatchEvent 方法的代码。
img到这里事件注册就完成了。
开始事件的存储,在 React 里所有事件的触发都是通过 dispatchEvent 方法统一进行派发的,而不是在注册的时候直接注册声明的回调。
React 把所有的事件和事件类型以及 React 组件进行关联,把这个关系保存在一个 Map 里面,然后在事件触发的时候根据当前的组件id 和事件类型找到对应的事件的回调函数。
img综合源码:
function enqueuePutListener(inst, registrationName, listener, transaction) {
大致的流程是执行完 listenTo(事件注册),然后执行 putListener 方法进行事件存储,所有的事件都会存储到一个对象中 -- listenerBank,具体由 EventPluginHub 进行管理。
//拿到组件唯一标识 id
listenerBank 其实就是一个二级 Map,这样的结构更加方便事件的查找。
这里的组件id 就是组件的唯一标志,然后和 fn 进行关联,再触发阶段就可以找到相关的事件回调。
img没看错,虽然我一直称呼为 Map,但其实就是一个我们平常使用的 object。
补充一个详细的完整流程图:
img在事件注册阶段,最终所有的事件和事件类型都会保存到 listenerBank 中。
再触发阶段,我们通过这个对象进行事件的查找,然后执行回调函数。
举个例子
//...省略
当我们点击 child div 的时候,会同时触发 father 的事件
img进入统一的事件分发函数(dispatchEvent)。
当我点击 child div 的时候,这个时候浏览器会捕获到这个事件,然后经过冒泡,事件会冒泡到 document 上,交给统一事件处理函数 dispatchEvent 进行处理。
img结合原生事件找到当前节点对应的 ReactDOMComponent 对象,在原生事件对象内已经保留了对应的 ReactDOMComponent 实例引用,应该是在挂载阶段就已经保存。
img看下 ReactDOMComponent 实例的内容:
img事件的合成,冒泡的处理以及事件回调的查找都是在合成阶段完成的。
img根据当前事件类型找到对应的合成类,然后进行合成对象的生成
//进行事件合成,根据事件类型获得指定的合成类
在这一步会把原生事件对象挂载到合成对象的自身,同时增加事件的默认行为处理和冒泡机制。
/**
*
* @param {obj} dispatchConfig 一个配置对象 包含当前的事件依赖 ["topClick"],冒泡和捕获事件对应的名称 bubbled: "onClick",captured: "onClickCapture"
* @param {obj} targetInst 组件实例ReactDomComponent
* @param {obj} nativeEvent 原生事件对象
* @param {obj} nativeEventTarget 事件源 e.target = div.child
*/
下面是增加的默认行为和冒泡机制的处理方法,其实就是改变了当前合成对象的属性值,调用了方法后属性值为 true,就会组织默认行为或者冒泡。
//在合成类原型上增加preventDefault和stopPropagation方法
打印一下 emptyFunction 代码
img根据当前节点实力查找他的所有父级实例,并存入 path
/**
*
* @param {obj} inst 当前节点实例
* @param {function} fn 处理方法
* @param {obj} arg 合成事件对象
*/
path 就是一个数组,里面的元素是 ReactDOMComponent
img在 listenerBank 查找事件回调并合成到 event。
紧接着上面的代码
'bubbled', arg);
上面的代码会调用下面这个方法,在 listenerBank 中查找到事件回调,并存入合成事件对象。
/**EventPropagators.js
* 查找事件回调后,把实例和回调保存到合成对象内
* @param {obj} inst 组件实例
* @param {string} phase 事件类型
* @param {obj} event 合成事件对象
*/
function accumulateDirectionalDispatches(inst, phase, event) {
var listener = listenerAtPhase(inst, event, phase);
if (listener) {//如果找到了事件回调,则保存起来 (保存在了合成事件对象内)
event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);//把事件回调进行合并返回一个新数组
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);//把组件实例进行合并返回一个新数组
}
}
/**
* EventPropagators.js
* 中间调用方法 拿到实例的回调方法
* @param {obj} inst 实例
* @param {obj} event 合成事件对象
* @param {string} propagationPhase 名称,捕获capture还是冒泡bubbled
*/
function listenerAtPhase(inst, event, propagationPhase) {
var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
return getListener(inst, registrationName);
}
/**EventPluginHub.js
* 拿到实例的回调方法
* @param {obj} inst 组件实例
* @param {string} registrationName Name of listener (e.g. `onClick`).
* @return {?function} 返回回调方法
*/
getListener: function getListener(inst, registrationName) {
var bankForRegistrationName = listenerBank[registrationName];
if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
return null;
}
var key = getDictionaryKey(inst);
return bankForRegistrationName && bankForRegistrationName[key];
}
img
为什么能够查找到的呢?
因为 inst (组件实例)里有_rootNodeID,所以也就有了对应关系。
img到这里,合成事件对象生成完成,所有的事件回调一保存到合成对象中。
批量处理合成事件对象内的回调方法。
生成完合成事件对象后,调用栈回到了我们起初执行的方法内。
img//在这里执行事件的回调
img
到下面这一步中间省略了一些代码,只贴出主要的代码,下面方法会循环处理 合成事件内的回调方法,同时判断是否禁止事件冒泡。
img贴上最后的执行回调方法的代码
/**
*
* @param {obj} event 合成事件对象
* @param {boolean} simulated false
* @param {fn} listener 事件回调
* @param {obj} inst 组件实例
*/
img
最后react 通过生成了一个临时节点fakeNode,然后为这个临时元素绑定事件处理程序,然后创建自定义事件 Event,通过fakeNode.dispatchEvent方法来触发事件,并且触发完毕之后立即移除监听事件。
到这里事件回调已经执行完成,但是也有些疑问,为什么在非生产环境需要通过自定义事件来执行回调方法。可以看下上面的代码在非生产环境对 ReactErrorUtils.invokeGuardedCallback方法进行了重写。