
框架总览
- 😁 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 (
<div onClick={this.outClick}>
<button id="btn" onClick={this.innerClick}>点我</button>
</div>
)
}
}
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()方法,则剩下的监听函数将不会被执行。