> For the complete documentation index, see [llms.txt](https://onenesss-organization.gitbook.io/jj-xiaoce/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://onenesss-organization.gitbook.io/jj-xiaoce/react-jin-jie-shi-jian-zhi-nan/15.-yuan-li-pian-shi-jian-yuan-li-lao-ban-ben.md).

# 15. 原理篇-事件原理（老版本）

### 一前言

本章节，我们来好好聊一下 React 的事件系统。我想先问一个问题，你觉得 React 事件系统对开发者来说重要吗？

事实上，前端应用因为离用户最近，所以会有很多交互逻辑，就会有很多事件与之绑定。因此，学习 React 事件系统更有利于开发者合理处理这些事件。

通过本章节的学习，你将收获 React 事件系统流程原理，从而解决面试中关于 React 事件的诸多问题。

**请带着问题去阅读，效果更佳：**

* React 为什么有自己的事件系统？&#x20;
* 什么是事件合成 ？&#x20;
* 如何实现的批量更新？
* 事件系统如何模拟冒泡和捕获阶段？
* 如何通过 dom 元素找到与之匹配的fiber？
* 为什么不能用 return false 来阻止事件的默认行为？
* 事件是绑定在真实的dom上吗？如何不是绑定在哪里？
* V17 对事件系统有哪些改变？

**首先，我要大胆地说，在 React 应用中，我们所看到的React事件都是‘假’的！** 可能有的同学对我说的丈二和尚摸不着头脑，不过不要紧，我会一步步说它到底假在哪里？你要知道：

* 1 给元素绑定的事件，不是真正的事件处理函数。
* 2 在冒泡/捕获阶段绑定的事件，也不是在冒泡/捕获阶段执行的。
* 3 甚至在事件处理函数中拿到的事件源 e ，也不是真正的事件源 e 。

React 为什么要写出一套自己的事件系统呢？

首先，对于不同的浏览器，对事件存在不同的兼容性，React 想实现一个兼容全浏览器的框架， 为了实现这个目标就需要创建一个兼容全浏览器的事件系统，以此抹平不同浏览器的差异。

其次，v17 之前 React 事件都是绑定在 document 上，v17 之后 React 把事件绑定在应用对应的容器 container 上，将事件绑定在同一容器统一管理，防止很多事件直接绑定在原生的 DOM 元素上。造成一些不可控的情况。由于不是绑定在真实的 DOM 上，所以 React 需要模拟一套事件流：事件捕获-> 事件源 -> 事件冒泡，也包括重写一下事件源对象 event 。

最后，这种事件系统，大部分处理逻辑都在底层处理了，这对后期的 ssr 和跨端支持度很高。

本章节涉及到事件原理均为 `v16.13.1` ，对于v17以及未来版本放弃的功能，这里会一笔带过。

### 二独特的事件处理

#### 冒泡阶段和捕获阶段

```js
export default function Index(){
    const handleClick=()=>{ console.log('模拟冒泡阶段执行') } 
    const handleClickCapture = ()=>{ console.log('模拟捕获阶段执行') }
    return <div>
        <button onClick={ handleClick  } onClickCapture={ handleClickCapture }  >点击</button>
    </div>
}
```

* 冒泡阶段：开发者正常给 React 绑定的事件比如 onClick，onChange，默认会在模拟冒泡阶段执行。
* 捕获阶段：如果想要在捕获阶段执行可以将事件后面加上 Capture 后缀，比如 onClickCapture，onChangeCapture。

#### 阻止冒泡

React 中如果想要阻止事件向上冒泡，可以用 `e.stopPropagation()` 。

```js
export default function Index(){
    const handleClick=(e)=> {
        e.stopPropagation() /* 阻止事件冒泡，handleFatherClick 事件讲不在触发 */
    }
    const handleFatherClick=()=> console.log('冒泡到父级')
    return <div onClick={ handleFatherClick } >
        <div onClick={ handleClick } >点击</div>
    </div>
}
```

* React 阻止冒泡和原生事件中的写法差不多，当如上 handleClick上 阻止冒泡，父级元素的 handleFatherClick 将不再执行，但是底层原理完全不同，接下来会讲到其功能实现。

#### 阻止默认行为

React 阻止默认行为和原生的事件也有一些区别。

**原生事件：** `e.preventDefault()` 和 `return false` 可以用来阻止事件默认行为，由于在 React 中给元素的事件并不是真正的事件处理函数。**所以导致 return false 方法在 React 应用中完全失去了作用。**

**React事件** 在React应用中，可以用 e.preventDefault() 阻止事件默认行为，这个方法并非是原生事件的 preventDefault ，由于 React 事件源 e 也是独立组建的，所以 preventDefault 也是单独处理的。

### 三 事件合成

React 事件系统可分为三个部分：

* 第一个部分是事件合成系统，初始化会注册不同的事件插件。
* 第二个就是在一次渲染过程中，对事件标签中事件的收集，向 container 注册事件。
* 第三个就是一次用户交互，事件触发，到事件执行一系列过程。

#### 事件合成概念

首先需要弄清楚什么叫事件合成呢？

比如在整个 React 应用中只绑定一个事件：

```js
export default function Index(){
  const handleClick = () => {}
  return <div >
     <button onClick={ handleClick } >点击</button>
  </div>
}
```

上面在 button 元素绑定的事件中，没有找到 handleClick 事件。但是在 document 上绑定一个 onclick 事件,如下：

![1.jpg](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/24c4f82c7fd14436bbf20d5001baa406~tplv-k3u1fbpfcp-watermark.image)

于是如下将应用中再添加一个 input 并绑定一个 onChange 事件：

```js
export default function Index(){
  const handleClick = () => {}
  const handleChange =() => {}
  return <div >
     <input onChange={ handleChange }  />
     <button onClick={ handleClick } >点击</button>
  </div>
}
```

在 input上还是没有找到绑定的事件 handleChange ，但是 document 的事件如下：

![2.jpg](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c5faf37d6fd94bd89bea8ee131df9c09~tplv-k3u1fbpfcp-watermark.image)

多了 blur，change ，focus ，keydown，keyup 等事件。

如上可以作出的总结是：

* React 的事件不是绑定在元素上的，而是统一绑定在顶部容器上，在 v17 之前是绑定在 document 上的，在 v17 改成了 app 容器上。这样更利于一个 html 下存在多个应用（微前端）。
* 绑定事件并不是一次性绑定所有事件，比如发现了 onClick 事件，就会绑定 click 事件，比如发现 onChange 事件，会绑定 `[blur，change ，focus ，keydown，keyup]` 多个事件。
* React 事件合成的概念：React 应用中，元素绑定的事件并不是原生事件，而是React 合成的事件，比如 onClick 是由 click 合成，onChange 是由 blur ，change ，focus 等多个事件合成。

#### 事件插件机制

React 有一种事件插件机制，比如上述 onClick 和 onChange ，会有不同的事件插件 SimpleEventPlugin ，ChangeEventPlugin 处理，先不必关心事件插件做了些什么，只需要先记住两个对象。这个对于后续的了解很有帮助。

**第一个 registrationNameModules ：**

```js
const registrationNameModules = {
    onBlur: SimpleEventPlugin,
    onClick: SimpleEventPlugin,
    onClickCapture: SimpleEventPlugin,
    onChange: ChangeEventPlugin,
    onChangeCapture: ChangeEventPlugin,
    onMouseEnter: EnterLeaveEventPlugin,
    onMouseLeave: EnterLeaveEventPlugin,
    ...
}
```

registrationNameModules 记录了 React 事件（比如 onBlur ）和与之对应的处理插件的映射，比如上述的 onClick ，就会用 SimpleEventPlugin 插件处理，onChange 就会用 ChangeEventPlugin 处理。应用于事件触发阶段，根据不同事件使用不同的插件。

**｜--------问与答---------｜**\
问：为什么要用不同的事件插件处理不同的 React 事件?

答：首先对于不同的事件，有不同的处理逻辑；对应的事件源对象也有所不同，React 的事件和事件源是自己合成的，所以对于不同事件需要不同的事件插件处理。

**｜--------end---------｜**<br>

**第二个registrationNameDependencies**

```js
{
    onBlur: ['blur'],
    onClick: ['click'],
    onClickCapture: ['click'],
    onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
    onMouseEnter: ['mouseout', 'mouseover'],
    onMouseLeave: ['mouseout', 'mouseover'],
    ...
}
```

这个对象保存了 React 事件和原生事件对应关系，这就解释了为什么上述只写了一个 onChange ，会有很多原生事件绑定在 document 上。在事件绑定阶段，如果发现有 React 事件，比如 onChange ，就会找到对应的原生事件数组，逐一绑定。

### 四 事件绑定

接下来重点研究一下事件绑定阶段，所谓事件绑定，就是在 React 处理 props 时候，如果遇到事件比如 onClick ，就会通过 addEventListener 注册原生事件，讲解事件注册之前先来想一个问题，还是上述的 demo ，给元素绑定的事件 handleClick ，handleChange ，最后去了哪里呢？

```js
export default function Index(){
  const handleClick = () => console.log('点击事件')
  const handleChange =() => console.log('change事件)
  return <div >
     <input onChange={ handleChange }  />
     <button onClick={ handleClick } >点击</button>
  </div>
}
```

* 对于如上结构，最后 onChange 和 onClick 会保存在对应 DOM 元素类型 fiber 对象（ hostComponent ）的 memoizedProps 属性上，如上结构会变成这样。

![4.jpg](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d44abaf90b694fbc8e77f0675a69ecfb~tplv-k3u1fbpfcp-watermark.image)

接下来就是 React 根据事件注册事件监听器。

> react-dom/src/client/ReactDOMComponent.js

```js
function diffProperties(){
    /* 判断当前的 propKey 是不是 React合成事件 */
    if(registrationNameModules.hasOwnProperty(propKey)){
         /* 这里多个函数简化了，如果是合成事件， 传入成事件名称 onClick ，向document注册事件  */
         legacyListenToEvent(registrationName, document）;
    }
}
```

`diffProperties` 函数在 diff props 如果发现是合成事件( onClick ) 就会调用 legacyListenToEvent 函数。注册事件监听器。接下来看一下 `legacyListenToEvent` 是如何注册事件的。

> react-dom/src/events/DOMLegacyEventPluginSystem.js

```js
function legacyListenToEvent(registrationName，mountAt){
   const dependencies = registrationNameDependencies[registrationName]; // 根据 onClick 获取  onClick 依赖的事件数组 [ 'click' ]。
    for (let i = 0; i < dependencies.length; i++) {
    const dependency = dependencies[i];
    //  addEventListener 绑定事件监听器
    ...
  }
}
```

* 这个就是应用上述 registrationNameDependencies 对 React 合成事件，分别绑定原生事件的事件监听器。比如发现是 onChange ，那么取出 `['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange']` 遍历绑定。

**那么有一个疑问，绑定在 document 的事件处理函数是如上写的handleChange，handleClick 吗？**

答案是否定的，绑定在 document 的事件，是 React 统一的事件处理函数 dispatchEvent ，React 需要一个统一流程去代理事件逻辑，包括 React 批量更新等逻辑。

只要是 **React 事件触发，首先执行的就是 dispatchEvent** ，那么有的同学会问，dispatchEvent 是如何知道是什么事件触发的呢？实际在注册的时候，就已经通过 bind ，把参数绑定给 dispatchEvent 了。

比如绑定 click 事件

```js
const listener = dispatchEvent.bind(null,'click',eventSystemFlags,document) 
/* TODO: 重要, 这里进行真正的事件绑定。*/
document.addEventListener('click',listener,false) 
```

### 五 事件触发

#### 一次点击事件

为了让大家更清楚了解事件触发的流程，假设 DOM 结构是如下这样的：

```js
export default function Index(){
    const handleClick1 = () => console.log(1)
    const handleClick2 = () => console.log(2)
    const handleClick3 = () => console.log(3)
    const handleClick4 = () => console.log(4)
    return <div onClick={ handleClick3 }  onClickCapture={ handleClick4 }  >
        <button onClick={ handleClick1 }  onClickCapture={ handleClick2 }  >点击</button>
    </div>
}
```

如果上述点击按钮，触发点击事件，那么在 React 系统中，整个流程会是这个样子的：

**第一步：批量更新**

首先上面讲到执行 dispatchEvent ，dispatchEvent 执行会传入真实的事件源 button 元素本身。通过元素可以找到 button 对应的 fiber ，fiber 和原生 DOM 之间是如何建立起联系的呢？

React 在初始化真实 DOM 的时候，用一个随机的 key internalInstanceKey 指针指向了当前 DOM 对应的 fiber 对象，fiber 对象用 stateNode 指向了当前的 DOM 元素。

![D3A29E95-F235-417B-951C-A15AB2ABA391.jpg](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/273b4791827544ef82f475137f3657d4~tplv-k3u1fbpfcp-watermark.image)

接下来就是批量更新环节，批量更新在 state 章节已经讲过，这里就不说了，还没掌握的同学可以回去温习一下。

> react-dom/src/events/ReactDOMUpdateBatching.js

```js
export function batchedEventUpdates(fn,a){
    isBatchingEventUpdates = true; //打开批量更新开关
    try{
       fn(a)  // 事件在这里执行
    }finally{
        isBatchingEventUpdates = false //关闭批量更新开关
    }
}
```

第一阶段模型：

![5.jpg](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/caf96e356f7e46248a4c546cef46b2bc~tplv-k3u1fbpfcp-watermark.image)

**第二步：合成事件源**

接下来会通过 onClick 找到对应的处理插件 SimpleEventPlugin ，合成新的事件源 e ，里面包含了 preventDefault 和 stopPropagation 等方法。

第二阶段模型：

![6.jpg](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c5791b365ca346749df096f1111fb714~tplv-k3u1fbpfcp-watermark.image)

**第三步：形成事件执行队列**

在第一步通过原生 DOM 获取到对应的 fiber ，接着会从这个 fiber 向上遍历，遇到元素类型 fiber ，就会收集事件，用一个数组收集事件：

* 如果遇到捕获阶段事件 onClickCapture ，就会 unshift 放在数组前面。以此模拟事件捕获阶段。
* 如果遇到冒泡阶段事件 onClick ，就会 push 到数组后面，模拟事件冒泡阶段。
* 一直收集到最顶端 app ，形成执行队列，在接下来阶段，依次执行队列里面的函数。

```js
 while (instance !== null) {
    const {stateNode, tag} = instance;
    if (tag === HostComponent && stateNode !== null) { /* DOM 元素 */
        const currentTarget = stateNode;
        if (captured !== null) { /* 事件捕获 */
            /* 在事件捕获阶段,真正的事件处理函数 */
            const captureListener = getListener(instance, captured); // onClickCapture
            if (captureListener != null) {
            /* 对应发生在事件捕获阶段的处理函数，逻辑是将执行函数unshift添加到队列的最前面 */
                dispatchListeners.unshift(captureListener);
                
            }
        }
        if (bubbled !== null) { /* 事件冒泡 */
            /* 事件冒泡阶段，真正的事件处理函数，逻辑是将执行函数push到执行队列的最后面 */
            const bubbleListener = getListener(instance, bubbled); // 
            if (bubbleListener != null) {
                dispatchListeners.push(bubbleListener); // onClick
            }
        }
    }
    instance = instance.return;
}
```

那么如上点击一次按钮，4个事件执行顺序是这样的：

* 首先第一次收集是在 button 上，handleClick1 冒泡事件 push 处理，handleClick2 捕获事件 unshift 处理。形成结构 `[ handleClick2 , handleClick1 ]`
* 然后接着向上收集，遇到父级，收集父级 div 上的事件，handleClick3 冒泡事件 push 处理，handleClick4 捕获事件 unshift 处理。`[handleClick4, handleClick2 , handleClick1,handleClick3 ]`
* 依次执行数组里面的事件，所以打印 4 2 1 3。

第三阶段模型：

![7.jpg](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f361b1a9c38646c698ae12b8befa8535~tplv-k3u1fbpfcp-watermark.image)

#### React如何模拟阻止事件冒泡

那么 React 是如何阻止事件冒泡的呢。来看一下事件队列是怎么执行的。

> legacy-events/EventBatching.js

```js
function runEventsInBatch(){
    const dispatchListeners = event._dispatchListeners;
    if (Array.isArray(dispatchListeners)) {
    for (let i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) { /* 判断是否已经阻止事件冒泡 */
        break;
      }    
      dispatchListeners[i](event) /* 执行真正的处理函数 及handleClick1... */
    }
  }
}
```

对于上述队列 `[handleClick4, handleClick2 , handleClick1, handleClick3 ]`

* 假设在上述队列中，handleClick2 中调用 `e.stopPropagation()`，那么事件源里将有状态证明此次事件已经停止冒泡，那么下次遍历的时候， `event.isPropagationStopped()` 就会返回 true ，所以跳出循环，handleClick1, handleClick3 将不再执行，模拟了阻止事件冒泡的过程。

### 六 总结

本章节把整个 React 事件系统主要流程讲了一遍，v17 版本相比 v16 改了一些东西，不过大体思路相差不大，希望看完能理解如下知识点，这在面试中是常考的：

* 1 什么是事件合成。
* 2 如何模拟事件捕获和事件冒泡阶段。
* 3 如何处理事件源对象。
* 4 一次点击到事件执行都发生了什么？
