前端心得:從 jQuery 到 React hook

jQuery 與 React 的差異

一個計數器案例

假設有一個計數器按鈕,它顯示的初始值是0,每次點擊,顯示數值都會加1:

// index.html
<button id="counter">0</button>

如果我們用jQuery,代碼大概會是這樣:

// index.js
$('#counter').on('click', function() {
  $(this).html($(this).html() * 1 + 1)
})

上面的代碼其實有三個步驟:

  1. 點擊發生時,我們從按鈕文本中讀取當前狀態值:$(this).html() * 1
  2. 計算新狀態值:$(this).html() * 1 + 1
  3. 更新HTML:$(this).html($(this).html() * 1 + 1)

我們來改寫下jQuery 代碼,以便更清晰地看到整個流程:

// 获取当前状态值
let currentCounter = $(this).html() * 1
// 计算新状态值
let nextCounter = currentCounter + 1
// 更新 HTML
$(this).html(nextCounter)

如果改用React,我們的代碼大致是這樣寫:

// react class component
// Counter.js
import React from "react";

export class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
	
  incrementCount = () => {
    this.setState(
      { count: this.state.count + 1 }
    );
  };
	
  render() {
    return (
      <h1 onClick={ this.incrementCount }>
	Clicks: { this.state.count }
      </h1>
    );
  }
}

ReactDOM.render(<Counter />, document.getElementById('root'));
// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
真相只有一個!《名偵探柯南》日本票房超越《Avengers : Infinity War》
那麼,相比之下,React 優於jQuery 的地方是哪些?為什麼當下整個前端的趨勢是從jQuery 遷移到React 等等框架的?

更新HTML

對比jQuery 與React 的代碼,我們可以看到,二者都需要獲取當前狀態值,也都需要計算新的狀態值。但是在jQuery 下,我們是這樣更新HTML:

$(this).html(nextCounter)

而 React 代碼裡,我們的 HTML 是個 JSX 模板,我們只要設定新狀態值,React 就會幫我們填充數據。

<button>{counter}</button>

React 重要觀念:畫面呈現與 state 必須一致!

封裝

jQuery 會將 HTML 與 JS 分開管理,如上面 counter 的例子,如果複雜一點的應用程式,就會難以維護。單單只看 index.html 這個檔案,你完全猜測不出來 counter 這個 id 到底隱含了什麼行為。

事實上,「顯示邏輯」和「模板」有很強的一致性,他們不應該被拆開。而擁有這兩者的物件,我們稱做他為元件( component )。將 JS 和類 HTML 語法混雜在一起,如上面的 react counter,可以直觀從單一檔案中看到 UI 與邏輯。

組件化

組件化建立在封裝上:

  1. 可組合(Composeable):一個組件易於和其它組件一起使用,或者嵌套在另一個組件內部。如果一個組件內部創建了另一個組件,那麼說父組件擁有它創建的子組件,通過這個特性,一個複雜的UI可以拆分成多個簡單的UI組件;
  2. 可重用(Reusable):每個組件都是具有獨立功能的,它可以被使用在多個UI場景;
  3. 可維護(Maintainable):每個小的組件僅僅包含自身的邏輯,更容易被理解和維護;

重繪與 Virtual DOM

一個應用程式的狀態會非常多,而且會頻繁的變化。如果我們傾聽某一個事件,就要針對這一個狀態進行 DOM 的處理,必須不斷增加 event listener。而在 React 中,他將這一件事的處理,拆成兩個關注點:

1. 當監聽某一個事件,先改變狀態( state )的資料,改變後會進行重繪
2. 針對所有資料,進行排版( JSX )

重繪將會改變所有的 DOM,而改變 DOM 是很耗效能的一件事。React 使用 Virtual DOM 的技術可以比對 state 的改變,最小程度地修改 DOM。

Virtual DOM computation
然而實務上 render 次數的控制還是 react app 效能的關鍵

React 解決的是複雜的 UI 管理 state 的難題

FB 推出 React 就是因為畫面管理極其困難,React 本身是單純的 UI 解決方案,結合 react-router-dom 和 redux 才算是完整的框架
圖片來源/ weibo
看完強大的 React 是否有種前端世界萬世太平的感覺?

React Class Component 的問題與 Hook 的出現

複雜的 component 變得很難理解

我們時常必須維護那些一開始非常簡單,但後來變成充滿無法管理的 stateful 邏輯和 side effect 的 component。每個 lifecycle 方法常常包含不相關的邏輯混合在一起。舉例來說,component 可能會在 componentDidMount 和 componentDidUpdate 中抓取資料。但是,同一個 componentDidMount 方法可能也包含一些設置 event listener 的不相關邏輯,並在 componentWillUnmount 執行清除它們。會一起改變且彼此相關的程式碼被拆分,但完全不相關的程式碼卻放在同一個方法裡。這讓它很容易製造 bug 和不一致性。

react lifecycle

在許多情況下,因為到處都是 stateful 邏輯,不可能把這些 component 拆分成更小的 component。而測試它們也很困難。這是許多人偏愛把 React 跟一個獨立的 state 管理函式庫( Redux )結合的其中一個理由。然而,這常常引入了太多的抽象,要求你在不同檔案間跳來跳去,而且讓重用 component 更加困難。

為了解決這個問題,Hook 讓你把一個 component 拆分成更小的 function,這基於什麼部分是相關的(像是設置一個 subscription 或是抓取資料),而不是強制基於 lifecycle 方法來分拆。你還可以選擇使用 reducer 來管理 component 的內部 state,使其更具可預測性。

class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...

可以發現設置document.title的邏輯是如何被分割到componentDidMountcomponentDidUpdate中的,訂閱邏輯又是如何被分割到componentDidMountcomponentWillUnmount中的。而且componentDidMount中同時包含了兩個不同功能的代碼。

那麼Hook如何解決這個問題呢?就像你可以使用多個state的Hook一樣,你也可以使用多個effect。這會將不相關邏輯分離到不同的effect中:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]);

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

可以在使用 Effect Hook 討論更多相關內容。

在 Component 之間重用 Stateful 的邏輯很困難

React 沒有提供一個方法來把可重用的行為「附加」到一個 component 上 (舉例來說,把它連結到一個 store)。如果你已經使用 React 一段時間,你或許會熟悉像是 render props 以及 higher-order components,這些試著解決這個問題的模式。但是這些模式要求你在使用它們時重新架構你的 component,這可能很麻煩,而且使程式碼更難追蹤。如果你在 React DevTools 上查看一個典型的 React 應用程式,你很可能會發現一個 component 的「包裝地獄」,被 provider、consumer、higher-order component、render props 以及其他抽象給層層圍繞。

下面是 render props 的範例:

class Index extends Component {
  render () {
    return (
      <div>
        <LikeButton
          wordings={{likedText: 'Liked', unlikedText: 'Like'}}
      </div>
    )
  }
}

class LikeButton extends Component {
  static defaultProps = {
    likedText: 'Cancel',
    unlikedText: 'Like'
  }

  constructor () {
    super()
    this.state = { isLiked: false }
  }

  handleClickOnLikeButton () {
    this.setState({
      isLiked: !this.state.isLiked
    })
  }

  render () {
    return (
      <button onClick={this.handleClickOnLikeButton.bind(this)}>
        {this.state.isLiked
          ? this.props.likedText
          : this.props.unlikedText} 
      </button>
    )
  }
}

使用 Hook,你可以從 component 抽取 stateful 的邏輯,如此一來它就可以獨立地被測試和重複使用。Hook 讓你不需要改變 component 階層就能重用 stateful 的邏輯。這讓在許多 component 之間共用或是與社群共用 Hook 很簡單。

以下是自訂 Hook 的範例:

import React, { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

詳細內容可以參考:打造你自己的 Hook 。

Class 讓人們和電腦同時感到困惑

除了使重用、組織程式碼更加困難以外,我們發現 class 可能是學習 React 的一大障礙。你必須了解 this 在 JavaScript 中如何運作,而這跟它在大部分程式語言中的運作方式非常不同。你必須記得 bind 那些 event handler。如果沒有不穩定的語法提案,撰寫的程式碼會非常繁瑣。

// react class component
export class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
	
  incrementCount = () => {
    this.setState(
      { count: this.state.count + 1 }
    );
  };
	
  render() {
    return (
      <h1 onClick={ this.incrementCount }>
	Clicks: { this.state.count }
      </h1>
    );
  }
}
// react hooks component
function Counter() {
  const [counter, setCounter] = useState(0)
  return <button onClick={() => setCounter(counter + 1)}>{counter}</button>
}

hooks 優缺點比較

優點

  • 減少了解太過多餘的元件週期,只要控制好 useEffect 即可
  • 用相對簡單的寫法解決複雜的問題,這點尤其重要

缺點

  • useEffect 由於把三個元件狀態合在一起,導致寫法太過簡單,也因為合在一起的關係所以使用時要注意,如果沒有加上限制就容易造成不停的觸發
React hook 的出現真是太棒惹嗚嗚

參考資料:
1. from jQuery to React : https://blog.zfanw.com/from-jquery-to-react/
2. react 特色 : https://medium.com/4cats-io/2016-%E5%B9%B4%E3%81%AE%E5%89%8D%E7%AB%AF-%E7%98%8B%E4%BB%80%E9%BA%BC-reactjs-4727a6ecc85a
3. react hook : https://zh-hant.reactjs.org/docs/hooks-intro.html

Share on facebook
Facebook
Share on linkedin
LinkedIn
Share on twitter
Twitter

More to explorer

Close Menu