Published on

React事件机制总结

Authors
  • avatar
    Name
    McDaddy(戣蓦)
    Twitter

前一篇写到了Vue和React的差异问题,其中一点关于React的事件机制。

原生事件都是被包装的,所有事件都是冒泡到顶层document监听,然后在这里合成事件下发

这段也是触及到了我这个React熟练工的盲区了。 今天决定仔细探究下这个问题。

原生事件 vs 合成事件

...
const btn = document.getElementById('native-btn');
btn.addEventListener('click', (e) => {
  // e.stopPropagation();
  console.log('原生click');
}, false);

const onClick = () => {
  console.log('合成click');
};

...
<button onClick={onClick}>普通按钮</button>
<button id="native-btn" onClick={onClick}>原生按钮</button>

定义两个button,第一个使用onClick属性,第二个在用了onClick的同时用原生的addEventListener方法绑定了一个click回调函数。

image-20200603153208791

查看第一个button元素,能看到click事件被注册在document/html/body/button上。

image-20200603153400634

查看第二个button元素,发现和第一个的区别是button本身被注册了两个click事件,一个是react自动加的,另一个是我们手动加的。

点击第二个button,得到输出

原生click
合成click

说明原生的事件是在合成事件前执行的,原因是原生的事件是在目标阶段执行的,也就是当捕获结束、冒泡之前(假设元素就是事件的currentTarget)的执行阶段,而合成事件是绑定在document的,所以比如需要冒泡到顶部才会执行。如果把e.stopPropagation()放开,得到的结果那就只有原生click被执行。

为了验证合成事件是注册在document, 我通过dev tool把普通按钮的document click绑定remove了。 结果就是点击无效了。反之如果只保留document的事件移除剩下的事件点击依然有效。

image-20200603154851209

React做了什么

  1. 每个合成事件里都有一个nativeEvent, 这个就是被包装的原生的事件。
image-20200603160018044
  1. 通过再上面的截图可以看到button元素除了有click事件,还被绑定了一系列的事件,如keyup/keydown等。假设这是一个input,原生元素需要失焦才会触发onchange, 而在react中只注册一个onChange事件也可以在每次键盘输入时触发。
  2. React在注册事件时做了浏览器兼容处理。
image-20200603160658534

事件注册

大致分为两步

  • 事件注册 - 组件挂载阶段,根据组件内的声明的事件类型 onclick,onchange 等,给 document 上添加事件 addEventListener,并指定统一的事件处理程序 dispatchEvent

  • 事件存储 - 就是把 react 组件内的所有事件统一的存放到一个对象里,缓存起来,为了在触发事件的时候可以查找到对应的方法去执行。

image-20200603161554936

image-20200603161956173

// 存储之后在listenerBank的结构,就是所有同类型事件都放在一起,通过nodeId去查找
{
    onClick:{
        nodeid1:()=>{...}
        nodeid2:()=>{...}
    },
    onChange:{
        nodeid3:()=>{...}
        nodeid4:()=>{...}
    }
}

事件执行

  1. 进入统一的事件分发函数(dispatchEvent)

  2. 结合原生事件找到当前节点对应的ReactDOMComponent对象

  3. 开始事件的合成

    3.1 根据当前事件类型生成指定的合成对象

    3.2 封装原生事件

    3.3 查找当前元素以及他所有父级, 如果同一类型事件在父子层级都定义了,那么React这里会模拟一个冒泡的过程,把所有回调都放在队列里,按冒泡的顺序先执行子再执行父

    3.4 在listenerBank查找事件回调并合成到 event queue(合成事件结束)

  4. 批量处理合成事件内的回调事件(事件触发完成 end)

image-20200603162228545

事件的异步处理

因为react用的是合成事件,为了节约内存都会在事件回调结束之后销毁这个事件,如果想要异步还能访问这个事件,需要调用persist接口。Event Pooling

export default () => {
  const onClick = e => {
    console.log(e.target.innerText);
    // 必须在这里用persist,否则下面的异步代码就会报错
    e.persist();
    setTimeout(() => {
      console.log("inner text in async", e.target.innerText);
    }, 300);
  };
  return (
    <>
      <div>react的event,如果在异步中访问会有问题</div>
      <Button onClick={onClick}>click me</Button>
    </>
  );
};

在线编辑

意义

  1. 解决IE的兼容问题,抹平浏览器的行为差异
  2. 减少内存消耗,提升性能,一种事件只在document上注册一次
  3. react的for循环渲染组件,不需要做事件代理,因为事件代理的两个优点(1. 节约内存 2. 动态对增删节点自动添加事件)React都做了

参考

【长文慎入】一文吃透 react 事件机制原理

【React深入】React事件机制