淺談 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


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