- Published on
React Event Model
- Authors

- Name
- pkl
- @2yyyyyy
事件流
在浏览器中,javascript 与HTML之间的交互是通过事件去实现的。在一整个页面的 DOM 结构中可能存在许许多多的事件,点击的click事件、加载的load事件、鼠标指针悬浮的mouseover事件等等。那么当用户在触发某个事件的时候,绑定在 DOM 中的事件是怎么执行的呢?
例如我们有如下的 DOM 结构:
<div>
<p>Click me!</p>
</div>
这就会涉及事件流的概念。事件流描述的是从页面中接受事件的机制。事件发生后会在目标节点和根节点之间按照特定的顺序传播,路径经过的节点都会接收到事件。
事件捕获
假设在节点p上触发了click事件,此时会产生两种事件传递顺序。Netscape提出一种事件流名为事件捕获(event capturing)。事件会从最外层开始发生,直到用户产生行为的元素。 针对上面的DOM结构,传递顺序如下:
document -> html -> body -> div -> p
最后到达用户触发的元素p,此时也被称为事件目标阶段。
事件冒泡
在到达事件目标阶段后,微软提出了名为事件冒泡(event bubbling)的事件流,也就是事件会从最内层的元素开始发生,一直向上传播,直到document对象。 针对我们上面的DOM结构,传递顺序如下:

1-4是捕获阶段,4-5是目标阶段,5-8是冒泡阶段。
addEventListener
DOM2级事件中规定的事件流同时支持了事件捕获阶段和事件冒泡阶段,而作为开发者,可以选择事件处理函数在哪一个阶段被调用。
addEventListener 方法用来为一个特定的元素绑定一个事件处理函数,是JavaScript中的常用方法。addEventListener有三个参数:
element.addEventListener(event, function, useCapture)
event,必须。字符串,指定事件名。所有 HTML DOM 事件,可以查看完整的 HTML DOM Event 对象参考手册。
function,必须。指定要事件触发时执行的函数。当事件对象会作为第一个参数传入函数。事件对象的类型取决于特定的事件。
useCapture,可选。布尔值,指定事件是否在捕获或冒泡阶段执行。可能值:true,事件在捕获阶段执行(即在事件捕获阶段调用处理函数);false,默认。事件在冒泡阶段执行(即表示在事件冒泡的阶段调用事件处理函数。
事件代理(委托)
利用事件流的特性,可以实现一种叫做事件代理的方法。 例如如下demo,有一些li元素,当点击每个li元素时,输出该元素的颜色:
<ul class="color_list">
<li>red</li>
<li>orange</li>
<li>yellow</li>
<li>green</li>
<li>blue</li>
<li>purple</li>
</ul>
<div class="box"></div>
<style>
.color_list {
display: flex;
display: -webkit-flex;
}
.color_list li {
width: 100px;
height: 100px;
list-style: none;
text-align: center;
line-height: 100px;
}
//每个li加上对应的颜色,此处省略
.box {
width: 600px;
height: 150px;
background-color: #cccccc;
line-height: 150px;
text-align: center;
}
</style>
如果想要在点击每个li标签时,输出li当中的颜色(innerHTML)。常规做法是遍历每个li,然后在每个li上绑定一个点击事件。
var color_list = document.querySelector('.color_list')
var colors = color_list.getElementsByTagName('li')
var box = document.querySelector('.box')
for (var n = 0; n < colors.length; n++) {
colors[n].addEventListener('click', function () {
console.log(this.innerHTML)
box.innerHTML = '该颜色为 ' + this.innerHTML
})
}
这种做法在li较少时可以使用,但如果有一万个li,那就会导致性能降低。这时候使用事件代理,利用事件流的特性,只绑定一个事件处理函数就可以完成:
function colorChange(e) {
var e = e || window.event //兼容性的处理
if (e.target.nodeName.toLowerCase() === 'li') {
box.innerHTML = '该颜色为 ' + e.target.innerHTML
}
}
color_list.addEventListener('click', colorChange, false)
由于事件冒泡机制,点击了li会冒泡到ul,此时就会触发绑定在ul上的点击事件,再利用target找到事件实际发生的元素,就可以达到预期的效果。
使用事件代理的好处不仅在于将多个事件处理函数减为一个,而且对于不同的元素可以有不同的处理方法。例如上述列表元素当中添加了其它的元素节点(如:a、span等),不必再一次循环给每一个元素绑定事件,直接修改事件代理的处理函数即可。
React的事件模型
在使用react的jsx来编写代码时,以点击事件为例,通常是这样定义的:
function App() {
return <div onClick={add}></div>
}
可以看到在jsx中定义的onClick明显不是DOM原生的事件名,这其实是react植根于浏览器事件模型实现的一套事件系统。react会将所有dom事件都绑定到document上面,而不是某一个元素上,统一的使用事件监听,并在冒泡阶段处理事件,所以当在挂载或销毁组件时,只需要在统一的事件监听上增加或者删除对象,当事件被触发时,组件会生成一个合成事件,然后传递到document中,document会通过dispatchEvent回调函数依次执行dispatchListener中同类型事件监听函数。
在react初始化时对事件进行挂载,react应用的入口函数:
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
在入口函数createRoot中,container参数是整个应用的跟DOM挂载点,在开始执行render函数时,为根DOM节点挂载自定义的click事件。
export function createRoot(container) {
const root = createContainer(container)
return {
render(element) {
// 挂载click事件
initEvent(container, 'click')
// ...省略
},
}
}
eventType只被允许传入支持的事件,首先定义一个事件列表:
const validEventTypeList = ['click']
export function initEvent(container, eventType) {
// 验证是否支持指定的事件类型
if (!validEventTypeList.includes(eventType)) {
console.warn('当前不支持', eventType, '事件')
return
}
// 绑定事件
container.addEventListener(eventType, (e) => {
dispatchEvent(container, eventType, e)
})
}
为根节点绑定事件,当事件被触发时,调用dispatchEvent回调函数依次执行dispatchListener中同类型事件监听函数。 既然要实现整个DOM的事件模型,自然也要实现原生DOM的整个事件流程,包括事件捕获和事件冒泡。 所以在dispatchEvent函数中需要完成的事件如下:
- 收集沿途的事件
- 构造合成事件
- 执行事件捕获列表
- 执行事件冒泡列表
function dispatchEvent(container, eventType, e) {
const targetElement = e.target
if (targetElement === null) {
console.warn('事件不存在target', e)
return
}
// 1. 收集沿途的事件
const { bubble, capture } = collectPaths(targetElement, container, eventType)
// 2. 构造合成事件
const se = createSyntheticEvent(e)
// 3. 遍历captue
triggerEventFlow(capture, se)
// 阻止冒泡事件执行
if (!se.__stopPropagation) {
// 4. 遍历bubble
triggerEventFlow(bubble, se)
}
}
收集沿途的事件
在一个事件被触发时,首先就要收集目标DOM节点及其所有父节点定义的所有事件,根据目标事件对象,查找所有父级onClick和onClickCapture事件,react默认的事件流方式是冒泡,如果想要在捕获阶段触发,可以添加onClickCapture来定义事件。 首先创建一个函数来映射事件名:
function getEventCallbackNameFromEventType(eventType) {
return {
click: ['onClickCapture', 'onClick'],
}[eventType]
}
getEventCallbackNameFromEventType函数通过接收一个DOM事件名来获取对应的捕获及冒泡的事件名列表。 为了方便获取节点中的属性,也就是根据事件名称获取对应节点的事件处理函数,使用一个自定义属性来保存所有的节点属性,并在初始化以及更新流程保存。
export const elementPropsKey = '__props'
// 接收节点和属性
export function updateFiberProps(node, props) {
node[elementPropsKey] = props
}
在render流程中处理props初始化以及更新是一个合适的时机。在completeWork函数的执行时对节点的props进行保存:
export const completeWork = (wip: FiberNode) => {
const newProps = wip.pendingProps;
const current = wip.alternate;
switch (wip.tag) {
case HostComponent:
if (current !== null && wip.stateNode) {
// update 更新流程
// 1. props是否变化 {onClick: xx} {onClick: xxx}
updateFiberProps(wip.stateNode, newProps);
} else {
// mount 初始化流程
// 1. 构建DOM
const instance = createInstance(wip.type, newProps);
// ...省略
}
return null;
case HostText:
// ...省略
return null;
case HostRoot:
// ...省略
case FunctionComponent:
// ...省略
default:
if (__DEV__) {
console.warn('未处理的completeWork情况', wip);
}
break;
}
};
- 初始化
初始化时直接在创建fiber节点的真实DOM时保存props。
export const createInstance = (type, props) => {
// 处理props
const element = document.createElement(type)
// 初始化时保存props
updateFiberProps(element, props)
return element
}
- 更新
节点的更新阶段直接将新的props重新保存。
updateFiberProps(wip.stateNode, newProps)
在收集事件的过程中使用节点的parentNode属性向上查找,直到根节点 定义两个数组,分别收集捕获与冒泡事件的处理函数,但是在逐层收集时,捕获事件使用unshift(前插),而冒泡使用push(后增)。这样做的原因是,在两个数组收集完毕后两者的顺序完全不同:
capture: [父-- > 子]
bubble: [子-- > 父]
capture父节点的事件处理函数在前,子节点的在后,bubble子节点的事件处理函数在前,父节点的在后。
在后续遍历这两个数组执行事件回调函数的时候,执行顺序正好与DOM事件流的触发顺序一致。先由父到子逐层捕获,再由子到父逐层冒泡。function collectPaths(targetElement, container, eventType) {
// 定义事件收集数组
const paths = {
capture: [],
bubble: [],
}
while (targetElement && targetElement !== container) {
// 收集
const elementProps = targetElement[elementPropsKey]
if (elementProps) {
// 获取事件映射
const callbackNameList = getEventCallbackNameFromEventType(eventType)
if (callbackNameList) {
callbackNameList.forEach((callbackName, i) => {
// 获取事件回调函数
const eventCallback = elementProps[callbackName]
if (eventCallback) {
if (i === 0) {
// capture
paths.capture.unshift(eventCallback)
} else {
// bubble
paths.bubble.push(eventCallback)
}
}
})
}
}
targetElement = targetElement.parentNode
}
return paths
}
构造合成事件
构造合成事件可以使我们更灵活的添加一些额外的处理逻辑。通过劫持事件对象一些原生的方法,添加额外的逻辑。 下面这段逻辑劫持了事件原生的阻止冒泡的方法stopPropagation,使用自定义属性__stopPropagation来控制是否阻止冒泡,然后重写stopPropagation方法。
function createSyntheticEvent(e: Event) {
const syntheticEvent = e;
// 初始化__stopPropagation
syntheticEvent.__stopPropagation = false;
// 保存原有的stopPropagation
const originStopPropagation = e.stopPropagation;
// 自定义stopPropagation执行,执行原有的stopPropagation方法
syntheticEvent.stopPropagation = () => {
syntheticEvent.__stopPropagation = true;
if (originStopPropagation) {
originStopPropagation();
}
};
return syntheticEvent;
}
执行事件流
执行事件流就是依次遍历定义的capture数组和bubble数组。当然,如果设置了自定义属性__stopPropagation为true,那么会阻止冒泡(阻止bubble数组的遍历执行)。
function triggerEventFlow(paths, se) {
for (let i = 0; i < paths.length; i++) {
const callback = paths[i]
// 执行事件处理函数
callback.call(null, se)
if (se.__stopPropagation) {
break
}
}
}