React 事件處理 (Event Handling)

在 React 中處理事件 (events),跟處理 DOM 事件很類似,但有些微的差異,像是:

  • HTML DOM 的 event handler (事件處理函式) 屬性 (attribute) 名稱都是小寫的,但 React 是 camelCase
  • HTML DOM 的 event handler 屬性的值是一個字串 (string),但在 JSX 中是一個函數 (function)
  • 在 React 的事件處理函式中,你不能像在 DOM 可以 return false 代表停掉瀏覽器預設行為,你需要明確的 call preventDefault()

像是在 HTML 中:

<button onclick="activateLasers()">
  Activate Lasers
</button>

而在 React 中:

<button onClick={activateLasers}>
  Activate Lasers
</button>

像是在 HTML 中:

<a href="#" onclick="console.log('The link was clicked.'); return false">
  Click me
</a>

而在 React 中:

function ActionLink() {
  function handleClick(e) {
    e.preventDefault();
    console.log('The link was clicked.');
  }

  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}

其中的 e 是一個 Event Object (精確的說是 React 的 SyntheticEvent wrapper),React 已經幫你處理好跨瀏覽器的問題了 (cross-browser compatibility),你只需要照 W3C 的標準語法!

第一次看 Event Handling 的語法,你可能會納悶,React 事件處理的寫法不就像是 DOM Level 0 的寫法嗎?對!沒錯,這就是 React 事件處理的語法;但這樣寫效能好嗎?放心!React 效能問題也幫你優化好了 (如果你在想 Event Delegation)。

在 React,你不用 addEventListener() 來綁定 DOM 元素的事件處理,你只需要在 JSX 直接加上元素的 Event Listener。

通常我們會將 Event Handler 寫成一個元件的方法 (Method),像是:

// 來寫一個可以 開/關 的按鈕
class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // 綁定 this
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick(e) {
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? '開' : '關'}
      </button>
    );
  }
}

ReactDOM.render(
  <Toggle />,
  document.getElementById('root')
);

點我看這個例子的結果

JavaScript 本質上其實只有 function 沒有所謂的 method,所以上面 handleClick() 方法裡面的 this 預設上是不會綁定任何物件的 (undefined),所以你要在 constructor() 中自己 bind 好 this。

但記得我們在這邊說過嗎?我們可以利用 Class Fields + Arrow Function 會自動綁定 this 的特性,幫我們省點力也讓 code 更簡潔。

你可以拿掉 constructor() 中的 bind,改寫 handleClick() 變成:

// Class Fields + Arrow Function 自動綁定 this 到元件本身
handleClick = (e) => {
  this.setState(prevState => ({
    isToggleOn: !prevState.isToggleOn
  }));
}

或也可以改寫 render():

render() {
  return (
    // arrow function 會綁定 this.handleClick() 執行時的環境
    // 跟 render() 的 this (元件自己) 同樣
    <button onClick={(e) => this.handleClick(e)}>
      {this.state.isToggleOn ? '開' : '關'}
    </button>
  );
}

後面這個寫法比較不好,因為每次元件 re-render 時,都要建立一個新的 callback function。

Event Handler 參數傳遞

你也可以傳客製化的參數給事件處理函數。

寫法像是:

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>

<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

<button onClick={() => this.deleteRow(a, b, c)}>Delete Row</button>

合成事件 (Synthetic Event) VS 原生事件 (Native Event)

React 有一層用來處理事件的機制,稱作 SyntheticEvent (合成事件),SyntheticEvent 幫你處理好跨瀏覽器的問題 (cross-browser compatibility),確保 React 的事件模型和 W3C 標準保持一致!

在綁定元件樹上的很多元件的很多事件上,React SyntheticEvent 利用了事件委任 (Event Delegation) 等技巧做了不同的效能優化,讓你可以用簡單直觀的語法來處理事件。而當元件從 DOM 中移除 (Unmount) 時,React 也會自動取消 (Unbind) 該元件綁定的事件。

在某些特殊的情況下,你要自己 addEventListener() 也可以,這就是所謂的原生事件 (Native Event)。Native Event 通常是在 componentDidMount() 中綁定,也需要在 componentWillUnmount() 中記得手動移除綁定 (removeEventListener())。

Native Event 的 Event Object 可以從 event.nativeEvent 取得,例如一般的 React (Synthetic) Event 我們可以用標準的 event.stopPropagation() 來阻止事件冒泡 (Event Bubbling),但如果是 Native Event,你則需要用 event.nativeEvent.stopImmediatePropagation()

Event Pooling

React 合成事件 (Synthetic Event) 的 Event Object 不能被非同步 (Asynchronous function) 的調用 (像是在 Promise 中),原因是 React 為了效能考量會在執行完 event callback 後,重複利用已經建立好的 SyntheticEvent object,這機制稱作 Event Pooling

例如:

function onClick(event) {
  console.log(event); // => nullified object.
  console.log(event.type); // => "click"
  const eventType = event.type; // => "click"

  setTimeout(function() {
    console.log(event.type); // => null
    console.log(eventType); // => "click"
  }, 0);

  // 錯誤的用法
  // 因為 this.state.clickEvent 的屬性隨後會變成 null values
  this.setState({clickEvent: event});

  // 但你可以直接傳遞 event object 的 property 沒問題
  // 因為 property 是 pass by value 不是 reference
  this.setState({eventType: event.type});
}
你如果想後續非同步的存取 event object 的屬性值,React 還是有提供解法,就是 call event.persist() 來告訴 React 保存這個 event object 給我獨用不要重複利用。