React事件委托机制详解

5,120 阅读4分钟

1 React16事件委托

1.1 document

React17之前的版本上,在虚拟dom上绑定的事件,都会被放入到一个事件池中

然后React全局只要给document绑定一此事件,在这个事件中监听事件冒泡事件

React再去事件池中找到对应的dom元素与对应的事件将其触发

1.2 验证

如何验证所有的React事件都绑定在document上?

这里有一个例子,在React注册之前document上绑定一个事件(保证自己的事件比React的document事件先触发),然后在真实的dom上绑定一个事件和一个React合成事件。

等待页面渲染完成之后,点击按钮验证

  • 准备一个React组件
export default function () {

  const buttonRef = useRef() as React.MutableRefObject<HTMLButtonElement>;

  useEffect(() => {
    buttonRef.current.addEventListener('click', function (e) {
      console.log('真实的button的dom事件')
    })
  }, [])

  const handleClick = useCallback(() => {
    console.log('绑定在虚拟dom上的button事件')
  }, [])

  return <button ref={buttonRef} onClick={handleClick}>按钮</button>

}

然后在React依赖插入之前在document上绑定一个事件

之后点击按钮,发现控制台打印如下

真实的button的dom事件
document上的事件
绑定在虚拟dom上的button事件

这就能验证React把所有事件全部挂载到document上这个机制了


甚至,可以在真实事件中阻止事件冒泡,然后就会发现,document事件和React的事件全部都没有触发

点击按钮后,控制台打印如下

真实的button的dom事件

1.3 如何实现

当事件冒泡到document上之后,在事件触发的回调Event中,有一个path参数

里面详细的描述了冒泡顺序

1.4 函数触发参数

可以看下React中事件绑定回调 和 真实dom绑定回调的对象区别

button.addEventListener('click', function (e) {
  console.log(e)
})
<button 
  id="button"
  onClick={(e)=>{
    console.log(e)
  }}
>
  按钮
</button>

可以看到,两者的回调是不同的

TypeScript 中,这两者的类型也是不同的

// 真实dom
button.addEventListener('click', function (e: MouseEvent) {})

// React合成事件
function(e:React.MouseEvent<HTMLButtonElement>){}

如果想在合成事件参数中获取原生的MouseEvent,可以访问这个参数

e.nativeEvent

1.5 persist

React为了把事件绑定性能做到了极致,做了一件事,当绑定的事件结束之后

合成事件回调对象 就会被立马销毁。下面的例子可以证明此事

const handleClick = useCallback((e) => {
  console.log(e)
  setTimeout(() => console.log(e))
}, [])

可以在事件执行中调用 e.persist 来保存状态

const handleClick = useCallback((e) => {
  e.persist();
  console.log(e)
  setTimeout(() => console.log(e))
}, [])

2 React中的this

2.1 this消失

说完合成事件之后,再看一下为什么在 class 组件中为什么事件都要进行绑定 this

首先,在js中,this指向其调用者,如下

const obj = {
  name: 'obj',
  func() {
    console.log(this)
  }
}

obj.func()  // {name: "obj", func: ƒ}

如果把func提取出来执行,那么func在模块化或严格模式中打印的就是undefined

const func = obj.func;

func();   // undefined

那这个操作就很熟悉了,组件代码如下

class App extends Component {

  handleClick() {
    console.log('handleClick')
  }
  
  render() {
    return (
      <button onClick={this.handleClick}>按钮</button>
    );
  }

}

this.handleClick 中的 handleClick 被提取出来,放在一个事件池中 [......]

之后被触发,这个时候,this 早已经不是指向那个组件了

2.2 绑定this指向

在以前,this绑定一共又三种方案,并且三种方案都有利弊

  • 虚拟dom直接bind绑定
<button onClick={this.handleClick.bind(this)}>按钮</button>

缺陷: 每当组件更新,函数就会bind一次,影响性能

  • 构造函数绑定
constructor(){
	this.handleClick = this.handleClick.bind(this)
}

缺陷: 每个函数都要bind一次,导致代码冗余臃肿

  • 直接绑定一个匿名函数
<button onClick={()=>this.handleClick()}>按钮</button>

缺陷: 上面两个缺陷集合体

2.3 箭头函数

箭头函数的this指向永远指向其函数所在的位置的this指向,如下

const obj = {
  name: 'obj',
  func() {
    const exec = () => console.log(this);
    exec();
  }
}

obj.func();   // {name: "obj", func: ƒ}}

所以,在组件中可以这么写

class App extends Component {

  handleClick = () => {
    console.log('handleClick')
  }

  render() {
    return (
      <button onClick={this.handleClick}>按钮</button>
    );
  }

}

3 React 17事件委托

先将项目中的React版本升级到17版本

$ yarn add react@17.0.1 react-dom@17.0.1

还是举刚才 React 把 事件绑定在 document 上的例子验证,一摸一样,只是把 React 版本升级到17

React16的执行顺序如下

真实的button的dom事件
document上的事件
绑定在虚拟dom上的button事件

那么React17 的执行顺序变成了这样

真实的button的dom事件
绑定在虚拟dom上的button事件
document上的事件

这是因为 React17 支持在页面中共存多个 React版本 ,如果把所有的事件全部绑定在一共document上,便会出现问题

所以,在17版本上,事件委托不放在 document 上,而是放在执行的根节点上,如 #root

ReactDOM.render(
  <HelloWorld />,
  document.getElementById('root') as HTMLElement
);