Why React Hooks

rccoder發表於2019-02-17

一、前言

1.1 為何要優先使用 SFC(Stateless Function Component)

Stateless Function Component:

const App = (props) => (
  <div>Hello, {props.name}</div>
)
複製程式碼

Class Component:

class App extends React.Component {
  render() {
    return (
      <div>Hello, {this.props.name}</div>
    )
  }
}
複製程式碼

上面是兩個最簡單的 function component 和 class component 的對比,首先從行數上來看,3 << 7。

再看 babel 編譯成 es2015 後的程式碼:

Function Component:

"use strict";

var App = function App(props) {
  return React.createElement("div", null, "Hello, ", props.name);
};
複製程式碼

Class Component:

去除了一堆 babel helper 函式

"use strict";

var App =
/*#__PURE__*/
function (_React$Component) {
  _inherits(App, _React$Component);

  function App() {
    _classCallCheck(this, App);

    return _possibleConstructorReturn(this, _getPrototypeOf(App).apply(this, arguments));
  }

  _createClass(App, [{
    key: "render",
    value: function render() {
      return React.createElement("div", null, "Hello, ", this.props.name);
    }
  }]);

  return App;
}(React.Component);

複製程式碼

Function Component 僅僅是一個普通的 JS 函式,Class Component 因為 ES2015 不支援 class 的原因,會編譯出很多和 class 相關的程式碼。

同時因為 Function Component 的特殊性,React 底層或許可以做 更多的效能優化

總的來說,以下點:

  • 更容易閱讀和單測
  • 寫更少的程式碼,編譯出更加精簡的程式碼
  • React Team 可正對這種元件做更加的效能優化

1.2 惱人的 bind(this)

在 React Class Component 中,我們一定寫過很多這樣的程式碼

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
    	name: 'rccoder',
    	age: 22
    },
    this.updateName = this.updateName.bind(this);
    this.updateAge = this.updateAge.bind(this);
  }
  
  render() {
    <div onClick={this.updateName}	
</div>
  }
}
複製程式碼

當然這個錯不在 React,而在於 JavaScript 的 this 指向問題,簡單看這樣的程式碼:

class Animate {
  constructor(name) {
    this.name = name;
  }
  getName() {
    console.log(this);
    console.log(this.name)
  }
}

const T = new Animate('cat');
T.getName();  // `this` is Animate Instance called Cat

var P = T.getName;
P(); // `this` is undefined
複製程式碼

這個例子和上面的 React 如出一轍,在沒有 bind 的情況下這樣寫會 導致了 this 是 global this,即 undefined。

解決它比較好的辦法就是在 contructor 裡面 bind this。

在新版本的 ES 中,有 Public Class Fields Syntax 可以解決這個問題,即:

class Animate {
  constructor(name) {
    this.name = name;
  }
  getName = () => {
    console.log(this);
    console.log(this.name)
  }
}

const T = new Animate('cat');
T.getName();  // `this` is Animate Instance called Cat

var P = T.getName;
P(); // `this` is Animate Instance called Cat
複製程式碼

箭頭函式不會建立自己的 this,只會依照詞法從自己的作用域鏈的上一層繼承 this,從而會讓這裡的 this 指向恰好和我們要的一致。

即使 public class fileds syntax 藉助 arrow function 可以勉強解決這種問題,但 this 指向的問題依舊讓人 “恐慌”。

1.2 被廢棄的幾個生命周圍

React 有非常多的生命週期,在 React 的版本更新中,有新的生命週期進來,也有一些生命週期官方已經漸漸開始認為是 UNSAFE。目前被標識為 UNSAFE 的有:

  • componentWillMount
  • componentWillRecieveProps
  • componentWillUpdate

新引入了

  • getDerivedStateFromProps
  • getSnapshotBeforeUpdate

getDerivedStateFromPropsgetSnapshotBeforeUpdate 均是返回一個處理後的物件給 componentDidUpdate,所有需要操作的邏輯都放在 componentDidUpdate 裡面。

原則上:

  • getDerivedStateFromProps + componentDidUpdate 可以替代 componentWillReceiveProps 的所有正常功能;
  • getSnapshotBeforeUpdate + componentDidUpdate 可以替代 componentWillUpdate 的所有功能。

具體的 原因遷移指南 可以參考 React 的官方部落格:Update on Async Rendering,有比較詳實的手把手指南。

最後,你應該依舊是同樣的感覺,Class Component 有如此多的生命週期,顯得是如此的複雜。


說了上面一堆看似和題目無關的話題,其實就是為了讓你覺得 “Function Component 大法好”,然後再開心的看下文。


二、什麼是 React Hooks

終於來到了和 Hooks 相關的部分,首先我們看下 什麼是 Hooks

2.1 什麼是 Hooks

首先來看下我們熟知的 WebHook:

Webhooks allow you to build or set up GitHub Apps which subscribe to certain events on GitHub.com. When one of those events is triggered, we'll send a HTTP POST payload to the webhook's configured URL. Webhooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. You're only limited by your imagination.

—— GitHub WebHook 介紹

核心是:When one of those events is triggered, we'll send a HTTP POST payload to the webhook's configured URL

2.2 什麼是 React Hooks

那 React Hooks 又是什麼呢?

Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.

看上去和 WebHook 區別不小,實際上也不小,怎麼解釋呢?

React Hook 在 Function Component 上開了一些 Hook,通過內建的一些 Hook 可以讓 Function Component 擁有自己的 state 和 生命週期,同時可以在 Hook 中操作它。此外通過組合內建的 Hook + 自己的業務邏輯 就可以生成新的 Custom Hook,以方便優雅複用一些業務邏輯。

三、Hooks 之前的一些問題

3.1 不同元件間邏輯複用問題

很多時候,檢視表現不同的元件都期望擁有一部分相同的邏輯,比如:強制登入、注入一些值等。這個時候我們經常會使用 HOC、renderProps 來包裝這層邏輯,總的來說都會加入一層 wrapper,使元件的層級發生了變化,隨著業務邏輯複雜度的增加,都會產生 wrapper 地獄的問題。

3.2 複雜元件閱讀困難問題

隨著業務邏輯複雜度的增加,我們的元件經常會在一個生命週期中幹多見事,比如:在 componentDidMount 中請求資料、傳送埋點等。總之就是在一個生命週期中會寫入多個完全不相關的程式碼,進而造成各種成本的隱形增加。

假如因為這些問題再把元件繼續抽象,不僅工作量比較繁雜,同時也會遇到 wrapper 地獄和除錯閱讀更加困難的問題。

3.3 class component 遇到的一些問題

從人的角度上講,class component 需要關心 this 指向等,大多經常在使用 function component 還是 class component 上感到困惑;從機器的角度上講,class component 編譯體積過大,熱過載不穩定

四、Hooks 有哪些功能

上述提到的三個問題,Hooks 某種意義上都做了自己的解答,那是如何解答的呢?

目前 Hooks 有: State Hook、Effect Hook、Context Hook、以及 Custom Hook。

4.1 State Hook

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
複製程式碼

useState 是 State Hook 的 API。入參是 initialState,返回一個 陣列,第一值是 state,第二個值是改變 state 的函式。

如果 initialState 的提高需要消耗大量的計算力,同時不期望這些計算阻塞後面要乾的事情的話。initialState 可以是個函式,會在 render 前呼叫達到 Lazy Calc 的效果。

useState(() => {
  // ... Calc
  return initialState;
})
複製程式碼

同時為了避免不必要的效能開銷,在設定 State 的時候如果兩個值是相等的,則也不會觸發 rerender。(判斷兩個值相等使用的是 Object.is

4.2 Effect Hook

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

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
    return () => {
      ... // Similar to componentWillUnMount。Named as clear up effect
    }

  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
複製程式碼

useEffect 相當於 class Component 中 componentDidMountcomponentDidUpdatecomponentWillUnmount 三個生命週期的大綜合,在元件掛載、更新、解除安裝的時候都會執行 effect 裡面的函式。用 “after render” 理解是最好的。

值的注意的是,Effect Hook 中的內容不會像 componentDidMountcomponentDidUpdate 一樣阻塞渲染。如果不期望這種表現,可是用來 API 表現一樣的 useLayoutEffect。(常見的計算器快速增加問題)

在一個 Function Component 裡,和 useState 一樣可以可以使用多次 useEffect,這樣在組織業務邏輯的時候,就可以按照業務邏輯去劃分程式碼片段了(而不是 Class Component 中只能按照生命週期去劃分程式碼片段)。

Effect Hook 的執行實際是 “after render”,為了避免每個 render 都執行所有的 Effect Hook,useEffect 提供了第二個入參(是個陣列),元件 rerender 後陣列中的值發生了變化後才會執行該 Effect Hook,如果傳的是個空陣列,則只會在元件第一次 Mount 後和 Unmount 前呼叫。這層優化理論上是可以在編譯時去做的,React Team 後期可能會做掉這層。

4.3 Custom Hook

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

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

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

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

  return isOnline;
}

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}
複製程式碼

useFriendStatus 就是一個典型的 Custom Hook,他利用 useState 和 useEffect 封裝了 訂閱朋友列表,設定朋友狀態,元件解除安裝時取消訂閱 的系列操作,最後返回一個表示是否線上的 state;在使用的時候,就可以像內建 Hook 一樣使用,享用封裝的系列邏輯。

在 Hooks 內部,即使在一個 Function Component 中,每個 Hooks 呼叫都有自己的隔離空間,能保證不同的呼叫之間互不干擾。

useFriendStatususe 開頭是 React Hooks 的約定,這樣的話方便標識他是一個 Hook,同時 eslint 外掛也會去識別這種寫法,以防產生不必要的麻煩。

同時如果 useXXXinitialState 是個變數,然後這個變數發生變化的時候,該 Hook 會自動取消之前的訊息訂閱,重新進行 Hooks 的掛載。也就是說,在上面的例子中,如果 props.friend.id 發生變化,useFriendStatus 這個 Hooks 會重新掛載,進而 online 狀態也會正常顯示。

4.4 Context Hook

function Example() {
  const locale = useContext(LocaleContext);
  const theme = useContext(ThemeContext);
  // ...
}
複製程式碼

useContext 的入參是某個 Provider 提供的 context,如果 context 發生變化的話,返回值也會立即發生變化。

4.5 Reduce Hook

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter({initialState}) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}
複製程式碼

如果 State 的變化有比較複雜的狀態流轉,可以使用 useReducer 讓他更加 Redux 化,以便讓這層邏輯更加清晰。同時 Reduce Hook 也提供 Lazy Calc 的功能,有需求的時候可以去使用它。

除此之外,內建的 Hooks 還有 useCallbackuseMemouseRefuseImperativeHandleuseLayoutEffectuseDebugValue。可以去 這裡 瞭解更多的用法。

五、例子對比

該例子是 Dan 在 React Conf 上的例子,算是非常有代表性的了:

視訊地址:React Today and Tomorrow and 90% Cleaner React With Hooks

同時這裡有一些 Hooks,看看實現和所解決的問題能更深的去了解 Hooks 的魅力:react hooks codesandbox

六、到底什麼時候會用到 React Hook

  • 更喜歡寫 Function Component
  • 想讓 Function Component 用有 state 和生命週期
  • 厭惡 class component 中按照 生命週期 去拆分程式碼區塊(而不是按照 業務邏輯 拆分程式碼區塊)
  • 想提煉不同 UI 元件的共有業務邏輯又不想因為 HOC 或者 renderProps 陷入 wrapper 地獄

七、引入的問題

7.1 奇怪的 useEffect

useEffect 可以覆蓋 componentDidMount,ComponentDidUpdate 和 componentWillUnmount 三個生命週期的操作,但從某種意義上說,實現 componentWillUnMount 的操作是有點讓人窒息的,如果是一個沒看過文件的人,絕對不知道要這麼操作。

useEffect(() => {
  // cDM or cDU
  return () => {
    // cWU
  }
})
複製程式碼

7.2 底層實現導致邏輯上的問題

React Hook 在內部實現上是使用 xxx,因為使用 React Hook 有兩個限制條件

  • 只能在頂層呼叫 Hooks,不能在迴圈、判斷條件、巢狀的函式裡面呼叫 Hooks。 這樣才能保證每次都是按照順序呼叫 Hooks,否則從 tuple 裡拿到的值就不一定是你要的那個 tuple 裡的值了,Effect 也是同樣的道理。具體原因大概是內部維護了一個佇列來表示 Hooks 執行的的順序,而順序正式定義的時候定義的,如果不再最頂層,可能會導致執行時 Hooks 的順序和定時時的不一致,從而產生問題,更加詳細的可以參考 React 官方的解釋:explanation
  • 只允許 Function Component 和 Custom Hooks 呼叫 React Hook,普通函式不允許呼叫 Hooks。

React Team 為此增加了 eslint 外掛:eslint-plugin-react-hooks,算是變通的去解決問題吧。

八、常見疑問

8.1 為什麼 useState 返回的是個陣列

這裡的陣列,從某種意義上說叫 tuple 會更加容器理解一些,可惜 JavaScript 中目前還沒這種概念。

同時,如果返回的是個 Object 又會怎麼樣呢?

let { state: name, setState: setName } = useState('Name');
let { state: surname, setState: setSurname } = useState('Surname');
複製程式碼

看起來好像是更糟糕的

8.2 效能問題

shouldComponentUpdate 使用 useMemo 即可,參考:How to memoize calculations?

同時 Hooks 的寫法避免的 Class Component 建立時大量建立示例和繫結事件處理的開銷,外加用 Hooks 的寫法可以避免 HOC 或者 renderProps 產生深層巢狀,對 React 來說處理起來會更加輕鬆。

8.3 能覆蓋 Class Component 的所有生命週期麼?

getSnapshotBeforeUpdatecomponentDidCatch 目前覆蓋不到

8.4 prevState 如何獲取

藉助 useRef 獲取

如果有其他問題,不妨去 React Hooks FQA 看看,大概率裡面涵蓋了你想知道的問題。

九、參考資料

原文地址:github.com/rccoder/blo… (去這交流更方便哦~)

相關文章