浅谈 React SyntheticEvent(合成事件)和它的坑

当你在 React 组件里看到以下代码的时候

<Button onClick={ 
    event =>{ 
     console.log('user click button') 
    }
  }> 
  Click Me</Button>

你是否曾经想过 onClick 的参数 event 是什么呢?它是浏览器 DOM 事件吗?

如果不是,为什么我们也能调用 event.stopPropagation() 或者 event.preventDefault() 等方法得到预期的效果呢?


答案:这是React在原生的DOM事件上的一层封装,称为SyntheticEvent(合成事件)


SyntheticEvent 是一类对象,它们都拥有以下属性/方法

boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
void persist()
DOMEventTarget target
number timeStamp
string type


SyntheticEvent 有两个主要特点:

1.兼容各种主流浏览器的DOM事件

这是 React 提供一个福利特性啊!我们不用操心这方面的浏览器兼容性问题了!



2. 事件池机制

如果你听说过线程池、Java字符串常量池等“池”的概念,你应该就能秒懂事件池的意思(编程理念很多数都是共通的)

事件池可以形象地理解为有个池子里装满了SyntheticEvent对象,程序有需要时会从池中取出一些使用,使用完后再放回池中。

事件池机制意味着 SyntheticEvent对象会被缓存且反复使用,目的是提高性能,减少创建不必要的对象。当SyntheticEvent对象被收回到事件池中时,属性会被抹除、重置为null。

因此,我们在写React事件回调函数的时候切记不能将 event 用于异步操作 —— 当异步操作真正执行的时候,SyntheticEvent对象有可能已经被重置了

反例如下:

import React, { Component } from "react";

class TextInput extends Component {
  state = {
    editionCounter: 0,
    value: this.props.defaultValue,
  }
  // 由于 setState 是异步操作,event.target.value 在运行时可能已经被重置了
  handleChange = event => 
    this.setState(prevState => ({ value: event.target.value, editionCounter: prevState.editionCounter + 1 }));

  render() {
    return (
      <span>Edited {this.state.editionCounter} times</span>
      <input
        type="text"
        value={this.state.value}
        onChange={this.handleChange} // WRONG!
      />
    )
  }
}

解决方案一: 使用 event.persist() 方法

persist 的直译过来是持久化,即 event.persist() 方法会将当前event踢出事件池,因此属性值可以一直存在而不会被重置。

import React, { Component } from "react";

class TextInput extends Component {
  state = {
    editionCounter: 0,
    value: this.props.defaultValue,
  }
  
  handleChange = event => {
    event.persist();  // 持久化
    this.setState(prevState => ({ value: event.target.value, editionCounter: prevState.editionCounter + 1 }));
  }

  render() {
    return (
      <span>Edited {this.state.editionCounter} times</span>
      <input
        type="text"
        value={this.state.value}
        onChange={this.handleChange}
      />
    )
  }
}


这种方案缺点很明显 —— 放弃了SyntheticEvent事件池的性能优势,使用不当的时候可能引起性能问题



解决方案二: 及时缓存所需的event属性值

所谓”缓存“,其实就是将对应的event属性值赋值给本地变量 —— 本地变量就没有被重置的危险了。

import React, { Component } from "react";

class TextInput extends Component {
  state = {
    editionCounter: 0,
    value: this.props.defaultValue,
  }
  
  handleChange = event => {
    const value = event.target.value; // value这个本地变量已经保存了目标值
    this.setState(prevState => ({ value, editionCounter: prevState.editionCounter + 1 }));
  }

  render() {
    return (
      <span>Edited {this.state.editionCounter} times</span>
      <input
        type="text"
        value={this.state.value}
        onChange={this.handleChange}
      />
    )
  }
}

题外话1:有些同学不喜欢定义临时变量,非常崇尚那种看起来非常”炫酷“的长长的一行代码;而有些同学热衷声明定义临时变量,让代码更具可读性,本文这种场景下,恰巧还能避免掉SyntheticEvent对象重置的这个坑。—— 关于声明临时变量这个话题,我偏向后者多一点,但是过多的临时变量有时又会降低可读性,难在把握好一个度。



总结

React SyntheticEvent 封装并兼容了浏览器DOM事件,采用了事件池的机制提升性能同时带来了不能异步使用的弊端 —— 还好有2个简单的解决方案。



题外话2:这个知识点面试会问吗?

一些技术氛围偏重的公司还是会问的,全面考察React的基础知识。

而如果是我作为面试官大概率是不会问的,个人认为这个知识点比较冷门,实际工作中普通业务开发下使用的不多。

但是,假如项目中需要跟事件大量打交道,比如支持大量用户手势事件如 视频播放、图片编辑 等,SyntheticEvent 还是需要深入学习的。

p.s. 多学(记)一点没坏处,考验脑容量的时候到了!


2229e33bd0d85d6dfe7b59b261757d3c.jpeg

参考文章:

https://reactjs.org/docs/events.html

https://medium.com/trabe/react-syntheticevent-reuse-889cd52981b6


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章