框架总览
- DOM事件流的三个阶段
- 关于React事件的疑问
- React事件绑定机制
- React事件和原生事件有什么区别
- React事件和原生事件的执行顺序,可以混用吗
- React事件如何解决跨浏览器兼容
- React stopPropagation 与 stopImmediatePropagation
- 从React的事件机制源码看整个流程
- 基本流程
- 事件注册
- 事件触发
- 总结
- 站在巨人肩上
DOM事件流的三个阶段
1、事件捕获阶段
当某个事件触发时,文档根节点最先接受到事件,然后根据DOM树结构向具体绑定事件的元素传递。该阶段为父元素截获事件提供了机会。
事件传递路径为:
window —> document —> boy —> div—> text
2、目标阶段
具体元素已经捕获事件。之后事件开始向根节点冒泡。
3、事件冒泡阶段
该阶段的开始即是事件的开始,根据DOM树结构由具体触发事件的元素向根节点传递。
事件传递路径:
text—> div —> body —> document —> window
使用addEventListener函数在事件流的的不同阶段监听事件。
DOMEle.addEventListener(‘事件名称’,handleFn,Boolean);
此处第三个参数Boolean即代表监听事件的阶段;
为true时,在在捕获阶段监听事件,执行逻辑处理;
为false时,在冒泡阶段监听事件,执行逻辑处理。
关于React事件的疑问
1.React事件绑定机制
考虑到浏览器的兼容性和性能问题,React 基于 Virtual DOM 实现了一个SyntheticEvent(合成事件)层,我们所定义的事件处理器会接收到一个SyntheticEvent对象的实例。与原生事件直接在元素上注册的方式不同的是,react的合成事件不会直接绑定到目标dom节点上,用事件委托机制,以队列的方式,从触发事件的组件向父组件回溯直到document节点,因此React组件上声明的事件最终绑定到了document 上。用一个统一的监听器去监听,这个监听器上保存着目标节点与事件对象的映射,当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做的好处:
1.减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 document 上注册一次
2.统一规范,解决 ie 事件兼容问题,简化事件逻辑
3.对开发者友好
React Event的主要四个文件是 ReactBrowerEventEmitter.js
(负责节点绑定的回调函数,该回调函数执行过程中构建合成事件对象,获取组件实例的绑定回调并执行,若有state变更,则重绘组件),ReactEventListener.js
(负责事件注册和事件分发), ReactEventEmitter
(负责事件的执行),EventPluginHub.js
(负责事件的存储)和ReactEventEmitterMixin.js
(负责事件的合成)。
2. React事件和原生事件有什么区别
带着问题用以下用代码来展示两者的区别:
- 点击button,最后的输出顺序是什么?
- B,G 处的type都是啥?
export default class Test extends React.Component {
componentDidMount() {
document.querySelector('#btn').addEventListener('click', (e) => {
console.log('A inner listener')
setTimeout(() => {
console.log('B inner listener timer', e.type)
})
})
document.body.addEventListener('click', (e) => {
console.log('C document listener')
})
window.addEventListener('click', (e) => {
console.log('D window listener')
})
}
outClick(e) {
setTimeout(() => {
console.log('E out timer', e.type)
})
console.log('F out e', e.type)
}
innerClick = (e) => {
console.log('G inner e',e.type)
e.stopPropagation()
}
render() {
return (
)
}
}
1. 最后的输出顺序为 A C G B
2. B处的type为click,而G处的type为null
响应过程(对应第一问)
我们参照上题,详细说一下事件的响应过程:
由于我们写的几个监听事件addEventListener,都没有给第三个参数,默认值为false,所以在事件捕获阶段,原生的监听事件没有响应,react合成事件只实现了事件冒泡。所以在捕获阶段没有事件响应。
接着到了事件绑定的阶段,button上挂载了原生事件,于是输出"A",setTimeout中的"B"则进入EVENT LOOP。在上一段中,我们提到react的合成事件是挂载到document上,所以“G”没有输出。
之后进入冒泡阶段,到了div上,与上条同理,不会响应outClick,继续向上冒泡。
之后冒泡到了document上,先响应挂载到document的原生事件,输出"c"。之后接着由里向外响应合成事件队列,即输出"G",由于innerClick函数内设置了e.stopPropagation()
。所以阻止了冒泡,父元素的事件响应函数没有执行。React合成事件执行e.stopPropagation()不会影响document层级之前的原生事件冒泡。但是会影响document之后的原生事件。所以没有执行body的事件响应函数
。之后再处理EVENT LOOP上的事件,输出'B''.
事件池(对应第二问)
在react中,合成事件被调用后,合成事件对象会被重用,所有属性被置为null
event.constructor.release(event);
所以题目中outClick中通过异步方式访问e.type是取不到任何值的,如果需要保留属性,可以调用event.persist()事件,会保留引用。
总结
(1)命名规范不同
React事件的属性名是采用驼峰形式的,事件处理函数是一个函数;
原生事件通过addEventListener给事件添加事件处理函数
(2)React事件只支持事件冒泡。原生事件通过配置第三个参数,true为事件捕获,false为事件冒泡
(3)事件挂载目标不同
React事件统一挂载到document上;
原生事件挂载到具体的DOM上
(4)this指向不同
原生事件:
1.如果onevent事件属性定义的时候将this作为参数,在函数中获取到该参数是DOM对象。用该方法可以获取当前DOM。
2在方法中直接访问this, this指向当前函数所在的作用域。或者说调用函数的对象。
React事件:
React中this指向一般都期望指向当前组件,如果不绑定this,this一般等于undefined。
React事件需要手动为其绑定this具体原因可以参考文章:
为什么需要在 React 类组件中为事件处理程序绑定 this
(5)事件对象不同
原生js中事件对象是原生事件对象,它存在浏览器兼容性,需要用户自己处理各浏览器兼容问题;
ReactJS中的事件对象是React将原生事件对象(event)进行了跨浏览器包装过的合成事件(SyntheticEvent)。
为了性能考虑,执行完后,合成事件的事件属性将不能再访问
React事件和原生事件的执行顺序,可以混用吗
由上面的代码我们可以理解:
react的所有事件都挂载在document中
当真实dom触发后冒泡到document后才会对react事件进行处理
所以原生的事件会先执行
然后执行react合成事件
最后执行真正在document上挂载的事件
不要将合成事件与原生事件混用。执行React事对象件的e.stopPropagation()可以阻止React事件冒泡。但是不能阻止原生事件冒泡;反之,在原生事件中的阻止冒泡行为,却可以阻止 React 合成事件的传播。因为无法将事件冒泡到document上导致的
React事件如何解决跨浏览器兼容
react事件在给document注册事件的时候也是对兼容性做了处理。
上面这个代码就是给document注册事件,内部其实也是做了对ie浏览器的兼容做了处理。
其实react内部还处理了很多,比如react合成事件:
React
根据 W3C 规范 定义了这个合成事件,所以你不需要担心跨浏览器的兼容性问题。
事件处理程序将传递 SyntheticEvent
的实例,这是一个跨浏览器原生事件包装器。 它具有与浏览器原生事件相同的接口,包括stopPropagation()
和 preventDefault()
,在所有浏览器中他们工作方式都相同。
每个SyntheticEvent
对象都具有以下属性:
属性名 | 类型 | 描述 |
---|---|---|
bubbles | boolean | 事件是否可冒泡 |
cancelable | boolean | 事件是否可拥有取消的默认动作 |
currentTarget | DOMEventTarget | 事件监听器触发该事件的元素(绑定事件的元素) |
defaultPrevented | boolean | 当前事件是否调用了 event.preventDefault()方法 |
eventPhase | number | 事件传播的所处阶段[0:Event.NONE-没有事件被处理,1:Event.CAPTURING_PHASE - 捕获阶段,2:被目标元素处理,3:冒泡阶段(Event.bubbles为true时才会发生)] |
isTrusted | boolean | 触发是否来自于用户行为,false为脚本触发 |
nativeEvent | DOMEvent | 浏览器原生事件 |
preventDefault() | void | 阻止事件的默认行为 |
isDefaultPrevented() | boolean | 返回的事件对象上是否调用了preventDefault()方法 |
stopPropagation() | void | 阻止冒泡 |
isPropagationStopped() | boolean | 返回的事件对象上是否调用了stopPropagation()方法 |
target | DOMEventTarget | 触发事件的元素 |
timeStamp | number | 事件生成的日期和时间 |
type | string | 当前 Event 对象表示的事件的名称,是注册事件的句柄,如,click、mouseover...etc. |
React
合成的SyntheticEvent
采用了事件池,这样做可以大大节省内存,而不会频繁的创建和销毁事件对象。
另外,不管在什么浏览器环境下,浏览器会将该事件类型统一创建为合成事件,从而达到了浏览器兼容的目的。
React stopPropagation 与 stopImmediatePropagation
React 合成事件与原生事件执行顺序图:
从图中我们可以得到一下结论:
(1)DOM 事件冒泡到document上才会触发React的合成事件,所以React 合成事件对象的e.stopPropagation,只能阻止 React 模拟的事件冒泡,并不能阻止真实的 DOM 事件冒泡
(2)DOM 事件的阻止冒泡也可以阻止合成事件原因是DOM 事件的阻止冒泡使事件不会传播到document上
(3)当合成事件和DOM 事件 都绑定在document上的时候,React的处理是合成事件应该是先放进去的所以会先触发,在这种情况下,原生事件对象的 stopImmediatePropagation能做到阻止进一步触发document DOM事件
stopImmediatePropagation
:如果有多个相同类型事件的事件监听函数绑定到同一个元素,则当该类型的事件触发时,它们会按照被添加的顺序执行。如果其中某个监听函数执行了 event.stopImmediatePropagation()方法,则剩下的监听函数将不会被执行。
从React的事件机制源码看整个流程
基本流程
在 react源码的 react-dom/src/events/ReactBrowserEventEmitter.js
文件的开头,有这么一大段注释:
/**
* Summary of `ReactBrowserEventEmitter` event handling:
*
* - Top-level delegation is used to ......
* ......
*
* +------------+ .
* | DOM | .
* +------------+ .
* | .
* v .
* +------------+ .
* | ReactEvent | .
* | Listener | .
* +------------+ . +-----------+
* | . +--------+|SimpleEvent|
* | . | |Plugin |
* +-----|------+ . v +-----------+
* | | | . +--------------+ +------------+
* | +-----------.--->|EventPluginHub| | Event |
* | | . | | +-----------+ | Propagators|
* | ReactEvent | . | | |TapEvent | |------------|
* | Emitter | . | |<---+|Plugin | |other plugin|
* | | . | | +-----------+ | utilities |
* | +-----------.--->| | +------------+
* | | | . +--------------+
* +-----|------+ . ^ +-----------+
* | . | |Enter/Leave|
* + . +-------+|Plugin |
* +-------------+ . +-----------+
* | application | .
* |-------------| .
* | | .
* | | .
* +-------------+ .
* .
* React Core . General Purpose Event Plugin System
*/
这段注释是在大概描述 React的事件机制,也就是这个文件中的代码要做的一些事情,大概意思就是说事件委托是很常用的一种浏览器事件优化策略,于是 React就接管了这件事情,并且还贴心地消除了浏览器间的差异,赋予开发者跨浏览器的开发体验,主要是使用 EventPluginHub这个东西来负责调度事件的存储,合成事件并以对象池的方式实现创建和销毁。
React内部事件系统实现可以分为两个阶段:事件注册
、事件触发
,涉及的主要类如下:
ReactEventListener
:负责事件注册和事件分发。React将DOM事件全都注册到document节点上,事件分发主要调用dispatchEvent
进行,从事件触发组件开始,向父元素遍历。
ReactEventEmitter
:负责每个组件上事件的执行。
EventPluginHub
:负责回调函数的存储
JSX中声明一个React事件,比如:
render() {
return (
)
}
用户点击button按钮触发click事件后,DOM将event传给ReactEventListener,触发document上注册的事件处理函数,执行ReactEventListener.dispatchEvent(event)
将事件分发到当前组件及以上的父组件。然后ReactEventEmitter
对每个组件进行事件的执行,先构造React合成事件,然后以队列的方式调用JSX中声明的callback。
备注:以下代码逻辑大部分写在注释里面
事件注册
这是 react
事件机制的第1步 - 事件注册,在这里你将了解react
事件的注册过程,以及在这个过程中主要经过了哪些关键步骤,同时结合源码进行验证和增强理解。
在这里并不会说非常细节的内容,而是把大概的流程和原理性的内容进行介绍,做到对整体流程有个认知和理解。
大致流程
react 事件注册过程其实主要做了2件事:事件注册、事件存储。
a. 事件注册 - 组件挂载阶段,根据组件内的声明的事件类型-onclick,onchange 等,给 document 上添加事件 -addEventListener,并指定统一的事件处理程序 dispatchEvent。
b. 事件存储 - 就是把 react 组件内的所有事件统一的存放到一个对象里,缓存起来,为了在触发事件的时候可以查找到对应的方法去执行。
关键步骤
上面大致说了事件注册需要完成的两个目标,那完成目标的过程需要经过哪些关键处理呢?
首先 react 拿到将要挂载的组件的虚拟 dom(其实就是 react element 对象),然后处理react dom
的 props ,判断属性内是否有声明为事件的属性,比如onClick,onChange
,这个时候得到事件类型 click,change
和对应的事件处理程序 fn
,然后执行后面3步
a. 完成事件注册
b. 将react dom
,事件类型,处理函数 fn
放入数组存储
c. 组件挂载完成后,处理 b 步骤生成的数组,经过遍历把事件处理函数存储到listenerBank(一个对象)
中
源码解析
1.从 jsx 说起
看个最熟悉的代码,也是我们日常的写法
//此处代码省略
handleFatherClick=()=>{
}
handleChildClick=()=>{
}
render(){
return
child
}
经过 babel
编译后,可以看到最终调用的方法是react.createElement
,而且声明的事件类型和回调就是个props
。
react.createElement('div',
{
className:'box',
},
react.createElement('div',
{
className:'father',
onClick: this.handleFatherClick
},
react.createElement('div',
{
className:'child',
onClick: this.handleChildClick,
},
'child'
);
);
);
react.createElement
执行的结果会返回一个所谓的虚拟 dom (react element object)
处理组件props,拿到事件类型和回调 fn
ReactDOMComponent
在进行组件加载(mountComponent)、更新(updateComponent)的时候,需要对props进行处理(_updateDOMProperties):
_updateDOMProperties: function (lastProps, nextProps, transaction) {
... // 前面代码太长,省略一部分
for (propKey in nextProps) {
var nextProp = nextProps[propKey];
if(propKey === STYLE){
...
}else if(registrationNameModules.hasOwnProperty(propKey)){
// 如果是props这个对象直接声明的属性,而不是从原型链中继承而来的,则处理它
// nextProp表示要创建或者更新的属性,而lastProp则表示上一次的属性
// 对于mountComponent,lastProp为null。updateComponent二者都不为null。unmountComponent则nextProp为null
if (nextProp) {
// mountComponent和updateComponent中,enqueuePutListener注册事件
enqueuePutListener(this, propKey, nextProp, transaction);
} else if (lastProp) {
// unmountComponent中,删除注册的listener,防止内存泄漏
deleteListener(this, propKey);
}
}else{
...
}
}
}
可以看下 registrationNameModules 的内容,就不细说了,他就是一个内置的常量。
事件注册和事件的存储
1.事件注册
接着上面的代码执行到了这个方法
enqueuePutListener(this, propKey, nextProp, transaction);
在这个方法里会进行事件的注册以及事件的存储,包括冒泡和捕获的处理
// inst: React Component对象
// registrationName: React合成事件名,如onClick
// listener: React事件回调方法,如onClick=callback中的callback
// transaction: mountComponent或updateComponent所处的事务流中,React都是基于事务流的
function enqueuePutListener(inst, registrationName, listener, transaction) {
if (transaction instanceof ReactServerRenderingTransaction) {
return;
}
var containerInfo = inst._hostContainerInfo;
var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
// 找到document
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
// 注册事件,将事件注册到document上
listenTo(registrationName, doc);
// 存储事件,放入事务队列中
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener
});
}
可以看到这个函数一共做了三件事:
1.根据当前的组件实例获取到最高父级-也就是document;
2.然后执行方法 listenTo
- 也是最关键的一个方法,进行事件注册。
- 最后执行transaction.getReactMountReady().enqueue,将react dom 实例,事件类型,处理函数 fn 组成一个对象放入数组存储。等待组件挂载后依次为数组里面每一项执行putListener。为数组每一项生成一个映射关系,把这个关系保存在了一个 map里,也就是一个对象(键值对),然后在事件触发的时候去根据当前的组件id和事件类型查找到对应的事件fn。
从ReactBrowserEventEmitter.listenTo;在ReactBrowserEventEmitter
.js文件下找到listenTo方法,可以发现它主要解决了不同浏览器间捕获和冒泡不兼容的问题。click,mousewheel等事件调用trapBubbledEvent
来注册冒泡事件;scroll,focus等事件调用trapCapturedEvent
来注册捕获事件。
listenTo : function (registrationName, contentDocumentHandle) {
var mountAt = contentDocumentHandle;
var isListening = getListeningForDocument(mountAt);
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') {
if (isEventSupported('wheel')) {
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'wheel', mountAt);
} else if (isEventSupported('mousewheel')) {
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'mousewheel', mountAt);
} else {
// Firefox needs to capture a different mouse scroll event.
// @see http://www.quirksmode.org/dom/events/tests/scroll.html
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'DOMMouseScroll', mountAt);
}
} else if (dependency === 'topScroll') {
if (isEventSupported('scroll', true)) {
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topScroll', 'scroll', mountAt);
} else {
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topScroll', 'scroll', ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE);
}
} else if (dependency === 'topFocus' || dependency === 'topBlur') {
if (isEventSupported('focus', true)) {
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topFocus', 'focus', mountAt);
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topBlur', 'blur', mountAt);
} else if (isEventSupported('focusin')) {
// IE has `focusin` and `focusout` events which bubble.
// @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topFocus', 'focusin', mountAt);
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topBlur', 'focusout', mountAt);
}
// to make sure blur and focus event listeners are only attached once
isListening.topBlur = true;
isListening.topFocus = true;
} else if (topEventMapping.hasOwnProperty(dependency)) {
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
}
isListening[dependency] = true;
}
}
}
最后执行EventListener.listen(冒泡)
或者EventListener.capture(捕获)
,来看看将事件绑定到冒泡阶段的具体代码:
// 三个参数为 topEvent、原生 DOM Event、Document(挂载节点)
trapBubbledEvent: function (topLevelType, handlerBaseName, element) {
if (!element) {
return null;
}
return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
}
// 三个参数为 Document(挂载节点)、原生 DOM Event、事件绑定函数
listen: function listen(target, eventType, callback) {
if (target.addEventListener) {
target.addEventListener(eventType, callback, false);
// 返回一个解绑的函数
return {
remove: function remove() {
target.removeEventListener(eventType, callback, false);
}
}
}
if (target.attachEvent) {
target.attachEvent('on' + eventType, callback);
// 返回一个解绑的函数
return {
remove: function remove() {
target.detachEvent('on' + eventType, callback);
}
}
}
}
也可以看到注册事件的时候也对 ie 浏览器做了兼容。
上面没有看到 dispatchEvent
的定义,其实上面代码中的callback统一为dispatchEvent
。dispatchEvent
将在之后讲。
到这里事件注册就完事儿了。
事件存储
开始事件的存储,在 react 里所有事件的触发都是通过 dispatchEvent
方法统一进行派发的,而不是在注册的时候直接注册声明的回调,来看下事件如何存储的 。
还是上面的源码:
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);//这个方法上面已说完
//这里涉及到了事务,事物会在以后的章节再介绍,主要看事件注册
//下面的代码是将putListener放入数组,当组件挂载完后会依次执行数组的回调。也就是putListener会依次执行
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,//组件实例
registrationName: registrationName,//事件类型 click
listener: listener //事件回调 fn
});
}
大致的流程就是执行完listenTo(事件注册)
,执行transaction.getReactMountReady().enqueue,将react dom 实例,事件类型,处理函数 fn 组成一个对象放入数组存储。等待组件挂载后依次为数组里面每一项执行putListener。为数组每一项生成一个映射关系,把这个关系保存在了一个 对象(键值对)里,这个对象叫做 listenerBank
,如下图。然后在事件触发的时候去根据当前的组件id和事件类型查找到对应的事件fn。
事件存储由EventPluginHub
来负责,EventPluginHub
在react事件系统的核心文件renderers/shared/event/EventPluginHub.js中定义,感兴趣的同学可以去看看源码~~
var EventPluginHub = {
injection,
putListener,
getListener,
deleteListener,
deleteAllListeners,
extractEvents, // 当顶层事件被触发,该方法中会传入原生事件,生成合成事件
enqueueEvents,// 合成事件进入事件队列
processEventQueue, // 调度事件队列上的所有合成事件
}
事件存储的入口在我们上面讲到的putListener方法,如下
function putListener() {
var listenerToPut = this;
EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
}
实际调用的是EventPluginHub.js中的putListener方法,该方法在组件挂载时执行。EventPluginHub.js主要负责事件的存储、合成事件以对象池的方式实现创建和销毁。
/**
* EventPluginHub用来存储React事件, 将listener存储到`listenerBank[registrationName][key]`
*
* @param {object} inst: 事件源
* @param {string} listener的名字,比如onClick
* @param {function} listener的callback
*/
//
var listenerBank = {};
putListener: function (inst, registrationName, listener) {
// 用来标识注册了事件,比如onClick的React对象。key的格式为'.nodeId', 只用知道它可以标示哪个React对象就可以了
var key = getDictionaryKey(inst);
var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
// 将listener事件回调方法存入listenerBank[registrationName][key]中,比如listenerBank['onclick'][nodeId]
// 所有React组件对象定义的所有React事件都会存储在listenerBank中
bankForRegistrationName[key] = listener;
//onSelect和onClick注册了两个事件回调插件, 用于walkAround某些浏览器兼容bug,不用care
var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
if (PluginModule && PluginModule.didPutListener) {
PluginModule.didPutListener(inst, registrationName, listener);
}
},
var getDictionaryKey = function (inst) {
return '.' + inst._rootNodeID;
};
listenerBank
其实就是一个二级 map
,这样的结构更方便事件的查找。
这里的组件 id 就是组件的唯一标识,然后和fn进行关联,在触发阶段就可以找到相关的事件回调。
看到这个结构是不是很熟悉呢?就是我们平常使用的 object.
到这里大致的流程已经说完,是不是感觉有点明白又不大明白。
没关系,再来个详细的图,重新理解下。
事件触发
在事件注册阶段,最终所有的事件和事件类型都会保存到listenerBank
中。
那么在事件触发的过程中上面这个对象有什么用处呢?
其实就是用来查找事件回调
事件触发过程总结为主要分为3个步骤:事件分发
、生成合成事件
、批量执行事件回调
1.进入统一的事件分发函数(dispatchEvent)
2.结合原生事件找到当前节点对应的ReactDOMComponent对象
3.开始事件的合成
3.1 根据当前事件类型生成指定的合成对象
3.2 封装原生事件和冒泡机制
3.3 查找当前元素以及他所有父级
3.4 在listenerBank查找事件回调并合成到 event(合成事件结束)
4.批量处理合成事件内的回调事件(事件触发完成 end)
举个栗子
在说具体的流程前,先看一个栗子,后面的分析也是基于这个栗子
handleFatherClick=(e)=>{
console.log('father click');
}
handleChildClick=(e)=>{
console.log('child click');
}
render(){
return
father
child
}
看到这个熟悉的代码,我们就已经知道了执行结果。
当我点击 child div 的时候,会同时触发father的事件。
1. 事件分发
当事件触发时,注册在document上的回调函数会被触发。事件触发的入口函数是ReactEventListener.dispatchEvent负责分发已经注册的回调函数。在这个函数中会调用batchingStrategy 的 batchUpdate 方法实现批量处理更新。batchUpdate以transaction形式调用,批量处理更新。
// topLevelType:带top的事件名,如topClick。不用纠结为什么带一个top字段,知道它是事件名就OK了
// nativeEvent: 用户触发click等事件时,浏览器传递的原生事件
dispatchEvent: function (topLevelType, nativeEvent) {
// disable了则直接不回调相关方法
if (!ReactEventListener._enabled) {
return;
}
// bookKeeping的作用看ta的定义就知道了,就是一个用来保存过程中会使用到的变量的对象。使用了
//react在源码中用到的对象池的方法来避免多余的垃圾回收
function TopLevelCallbackBookKeeping(topLevelType, nativeEvent) {
this.topLevelType = topLevelType;
this.nativeEvent = nativeEvent;
this.ancestors = [];
}
Object.assign(TopLevelCallbackBookKeeping.prototype, {
destructor: function() {
this.topLevelType = null;
this.nativeEvent = null;
this.ancestors.length = 0;
},
});
PooledClass.addPoolingTo(
TopLevelCallbackBookKeeping,
PooledClass.twoArgumentPooler
);
var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
try {
// 放入批处理队列中,React事件流也是一个消息队列的方式
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
}
关于getPooled,可以参考我的另一篇文章react 对象池解读
TopLevelCallbackBookKeeping是一个类,该类对象用于记录topLevelType,nativeEvent和用于存储所有的祖先节点数组ancestors(当前是空的,只有分发时才会遍历并存储所有的祖先节点) 。
那么传入batchedUpdates 内部的回调函数handleTopLevelImpl是什么呢???它其实就是事件分发的核心部分。
// document进行事件分发,这样具体的React组件才能得到响应。因为DOM事件是绑定到document上的
function handleTopLevelImpl(bookKeeping) {
// 获取发生原生的事件的e.target
var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
// // 获取原生事件的target说在的组件,它是虚拟DOM
var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
// 执行事件回调前,先由当前组件向上遍历它的所有父组件。得到ancestors这个数组。
// 因为事件回调中可能会改变Virtual DOM结构,所以要先遍历好组件层级
var ancestor = targetInst;
do {
bookKeeping.ancestors.push(ancestor);
// 这里的findParent曾经给我带来误导,我以为去找当前元素所有的父节点,但其实不是的,
// 我们知道一般情况下,我们的组件最后会被包裹在的标签里
// 一般是没有组件再去嵌套它的,所以通常返回null
ancestor = ancestor && findParent(ancestor);
} while (ancestor);
// 从当前组件向父组件遍历,依次执行注册的回调方法. 我们遍历构造ancestors数组时,是从当前组件向父组件回溯的,故此处事件回调也是这个顺序
// 这个顺序就是冒泡的顺序
for (var i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
}
}
function findParent(inst) {
while (inst._hostParent) {
inst = inst._hostParent;
}
var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst);
var container = rootNode.parentNode;
return ReactDOMComponentTree.getClosestInstanceFromNode(container);
}
从上面的事件分发中可见,React自身实现了一套冒泡机制。从触发事件的对象开始,向父元素回溯,依次调用它们注册的事件callback。
看下ReactDOMComponent实例的内容
事件处理由_handleTopLevel完成。它其实是调用ReactBrowserEventEmitter.handleTopLevel() ,如下
// React事件调用的入口。DOM事件绑定在了document原生对象上,每次事件触发,都会调用到handleTopLevel
handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
// 采用对象池的方式构造出合成事件。不同的eventType的合成事件可能不同
var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
// 批处理队列中的events
runEventQueueInBatch(events);
}
handleTopLevel方法是事件callback调用的核心。它主要做两件事情,一方面利用浏览器回传的原生事件构造出React合成事件,另一方面采用队列的方式处理events。先看如何构造合成事件。
2. 事件合成
合成事件时一个跨浏览器原生事件包装器,具有与浏览器原生事件相同的接口,包括 stopPropagation() 和 preventDefault() ,除了事件在所有浏览器中他们工作方式都相同。现在来看一看React是如何实现合成事件的。
EventPluginHub.js中extractEvents方法:
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);
if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
}
}
}
return events;
}
插件中的extractevents
注意不要将EventPluginHub.extractevents和possiblePlugin.extractEvents搞混了
以点击事件click的生成插件SimpleEventPlugin为例:
//进行事件合成,根据事件类型获得指定的合成类
var SimpleEventPlugin = {
eventTypes: eventTypes,
extractEvents: function extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
//代码已省略....
var EventConstructor;
switch (topLevelType) {
//代码已省略....
case 'topClick'://【这里有一个不解的地方】 topLevelType = topClick,执行到这里了,但是这里没有做任何操作
if (nativeEvent.button === 2) {
return null;
}
//代码已省略....
case 'topContextMenu'://而是会执行到这里,获取到鼠标合成类
EventConstructor = SyntheticMouseEvent;
break;
case 'topAnimationEnd':
case 'topAnimationIteration':
case 'topAnimationStart':
EventConstructor = SyntheticAnimationEvent;//动画类合成事件
break;
case 'topWheel':
EventConstructor = SyntheticWheelEvent;//鼠标滚轮类合成事件
break;
case 'topCopy':
case 'topCut':
case 'topPaste':
EventConstructor = SyntheticClipboardEvent;
break;
}
// 合成事件对象都是以pool方式创建和销毁的,这提高了React的性能,同时也意味着一旦事件执行结束
// 该合成事件对象会被销毁。因此不能通过异步方式获取该事件
var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
EventPropagators.accumulateTwoPhaseDispatches(event);
return event;//最终会返回合成的事件对象
}
EventPluginHub.extractEvents()方法是通过执行各个插件的extractEvents方法来创建合成事件。possiblePlugin.extractEvents()根据事件的Type创建不同插件的合成事件,accumulateInto()负责将所有插件的合成事件存入到events数组中,形成当前事件的合成事件集合。EventPluginRegistry.plugins默认包含五种plugin,他们是在EventPluginHub初始化阶段注入进去的,分别是 SimpleEventPlugin、EnterLeaveEventPlugin、ChangeEventPlugin、SelectEventPlugin和BeforeInputEventPlugin。根据不同的事件类型采用不同的事件合成方法,这些事件合成方法有SyntheticAnimationEvent.js、SyntheticFocusEvent.js、SyntheticKeyboardEvent.js、SyntheticMouseEvent.js、SyntheticTouchEvent.js、SyntheticUIEvent.js、SyntheticWheelEvent.js等等一共13种合成方法。
上面提到调用EventPropagators.accumulateTwoPhaseDispatches(event)
从EventPluginHub中获取回调函数,存储到合成事件的_dispatchListeners属性中。如下:
// EventPropagators.js
function accumulateTwoPhaseDispatches(events) {
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}
// forEachAccumulated 函数在接下来会讲到
function accumulateTwoPhaseDispatchesSingle(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
EventPluginUtils.traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
}
}
把所有父级元素绑定的相关事件按照捕获->冒泡的顺序存push到合成事件对象的_dispatchListeners属性中。该属性为一个数组。
/**
*
* @param {obj} inst 当前节点实例
* @param {function} fn 处理方法
* @param {obj} arg 合成事件对象
*/
function traverseTwoPhase(inst, fn, arg) {
var path = [];//存放所有实例 ReactDOMComponent
while (inst) {
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);//处理冒泡,从0开始处理,我们直接看冒泡
}
}
看下 path 长啥样
紧接着如何在listenerBank中查找事件回调并合成到合成对象的_dispatchListeners中呢。
紧接着上面代码
fn(path[i], '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];
}
为什么能够查找到的呢?
因为 inst (组件实例)里有_rootNodeID,所以也就有了对应关系。比如通过上面函数getDictionaryKey
获取到触发事件的DOM组件的_rootNodeId
属性,然后根据callback = listenerBank[eventType][_rootNodeId]
可以获取该组件的回调函数。
到这里事件合成对象生成完成,所有的事件回调已保存到了合成对象中。
3.批量执行事件回调
事件合成之后就需要执行事件回调函数,React以批量处理事件队列的方式执行事件的。它的入口函数是在ReactEventEmitterMixin.js中的runEventQueueInBatch方法:
var eventQueue = null;
function runEventQueueInBatch(events) {
EventPluginHub.enqueueEvents(events);
EventPluginHub.processEventQueue(false);
}
//EventPluginHub.enqueueEvents()方法是将合成事件写入队列,EventPluginHub.processEventQueue()方法是执行队列中的事件。
// EventPluginHub.enqueueEvents()方法的实现逻辑很简单,使用accumulateInto方法将events存入eventQueue队列中。
enqueueEvents: function (events) {
if (events) {
eventQueue = accumulateInto(eventQueue, events);
}
}
function accumulateInto(current, next) {
if (current == null) {
return next;
}
// 将next添加到current中,返回一个包含他们两个的新数组
// 如果next是数组,current不是数组,采用push方法,否则采用concat方法
// 如果next不是数组,则返回一个current和next构成的新数组
if (Array.isArray(current)) {
if (Array.isArray(next)) {
current.push.apply(current, next);
return current;
}
current.push(next);
return current;
}
if (Array.isArray(next)) {
return [current].concat(next);
}
return [current, next];
}
// EventPluginHub.processEventQueue()方法的实现逻辑分为:
processEventQueue: function (simulated) {
// 现将eventQueue 设置为null,以便在处理过程中让新合成事件入队列
var processingEventQueue = eventQueue;
eventQueue = null;
if (simulated) {
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
} else {
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
}
// This would be a good time to rethrow if any of the event handlers threw.
ReactErrorUtils.rethrowCaughtError();
}
(1)获取合成事件队列,其中可能包含之前没处理完的合成事件
(2)遍历合成事件队列,
function forEachAccumulated(arr, cb, scope) {
if (Array.isArray(arr)) {
arr.forEach(cb, scope);
} else if (arr) {
cb.call(scope, arr);
}
}
事件队列的回调函数为executeDispatchesAndReleaseSimulated,负责事件的分发和事件遍历结束后是否释放合成事件对象。
var executeDispatchesAndReleaseSimulated = function (e) {
return executeDispatchesAndRelease(e, true);
};
/**/
var executeDispatchesAndRelease = function (event, simulated) {
if (event) {
/*按存储顺序分发事件,先进先出*/
EventPluginUtils.executeDispatchesInOrder(event, simulated);
/*判断是否释放事件*/
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
var executeDispatchesAndReleaseTopLevel = function (e) {
return executeDispatchesAndRelease(e, false);
};
var executeDispatchesAndRelease = function (event, simulated) {
if (event) {
//进行事件分发
EventPluginUtils.executeDispatchesInOrder(event, simulated);
if (!event.isPersistent()) {
// 处理完,则release掉event对象,采用对象池方式,减少GC
// React帮我们处理了合成事件的回收机制,不需要我们关心。但要注意,如果使用了DOM原生事件,则要自己回收
event.constructor.release(event);
}
}
};
// EventPluginUtils.js
// 事件处理的核心
function executeDispatchesInOrder(event, simulated) {
var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._dispatchInstances;
if (Array.isArray(dispatchListeners)) {
// 如果有多个listener,则遍历执行数组中event
for (var i = 0; i < dispatchListeners.length; i++) {
// 如果isPropagationStopped设成true了,则停止事件传播,退出循环。
if (event.isPropagationStopped()) {
break;
}
// 执行event的分发,从当前触发事件元素向父元素遍历
// event为浏览器上传的原生事件
// dispatchListeners[i]为JSX中声明的事件callback
// dispatchInstances[i]为对应的React Component
executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
}
} else if (dispatchListeners) {
// 如果只有一个listener,则直接执行事件分发
executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
}
// 处理完event,重置变量。因为使用的对象池,故必须重置,这样才能被别人复用
event._dispatchListeners = null;
event._dispatchInstances = null;
}
通过executeDispatchesInOrder
函数可知,dispatch 合成事件分为两个步骤:
- 通过
_dispatchListeners
里得到所有绑定的回调函数,在通过_dispatchInstances
的绑定回调函数的虚拟dom元素 - 循环执行
_dispatchListeners
里所有的回调函数,这里有一个特殊情况,也是react阻止冒泡的原理
当回调函数里使用了stopPropagation
会使得数组后面的回调函数不能执行,这样就做到了阻止事件冒泡
目前还是还有看到执行事件的代码,在接着看:
/**
*
* @param {obj} event 合成事件对象
* @param {boolean} simulated false
* @param {fn} listener 事件回调
* @param {obj} inst 组件实例
*/
function executeDispatch(event, simulated, listener, inst) {
var type = event.type || 'unknown-event';
event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
if (simulated) {//调试环境的值为 false,按说生产环境是 true
//方法的内容请往下看
ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
} else {
//方法的内容请往下看
ReactErrorUtils.invokeGuardedCallback(type, listener, event);
}
event.currentTarget = null;
}
/** ReactErrorUtils.js
* @param {String} name of the guard to use for logging or debugging
* @param {Function} func The function to invoke
* @param {*} a First argument
* @param {*} b Second argument
*/
var caughtError = null;
function invokeGuardedCallback(name, func, a) {
try {
func(a);//直接执行回调方法
} catch (x) {
if (caughtError === null) {
caughtError = x;
}
}
}
var ReactErrorUtils = {
invokeGuardedCallback: invokeGuardedCallback,
invokeGuardedCallbackWithCatch: invokeGuardedCallback,
rethrowCaughtError: function rethrowCaughtError() {
if (caughtError) {
var error = caughtError;
caughtError = null;
throw error;
}
}
};
if (process.env.NODE_ENV !== 'production') {//非生产环境会通过自定义事件去触发回调
if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof document !== 'undefined' && typeof document.createEvent === 'function') {
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);
};
}
}
复制代码
最后react 通过生成了一个临时节点fakeNode,然后为这个临时元素绑定事件处理程序,然后创建自定义事件 Event,通过fakeNode.dispatchEvent方法来触发事件,并且触发完毕之后立即移除监听事件。
总结
React的事件系统主要分为3个步骤:
一是事件绑定,ReactBrowserEventEmitter的trapBubbledEvent等方法为节点或文档绑定事件;
二是事件监听,ReactEventListener.dispatchEvent将把该回调函数分发给事件对象的_dispatchListener属性;调用ReactBrowserEventEmitter.ReactEventListener方法以监听节点事件。
三是事件分发与触发,对触发的事件进行分发,并创建合成事件对象,在回调中用构建合成事件对象并执行合成事件对的象绑定回调。
站在巨人肩上
一看就晕的React事件机制
揭秘React形成合成事件的过程
【长文慎入】一文吃透 react 事件机制原理
React 事件系统
React事件机制 - 源码概览(上)
react 事件池
React 合成事件和原生事件的区别
React的事件机制
React event实现原理
为什么需要在 React 类组件中为事件处理程序绑定 this
React合成事件系统
【译】了解React源代码-UI更新(DOM树)9