Why React Hooks
未完,待續。原文地址:github.com/rccoder/blog/issues/33
一、前言
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
getDerivedStateFromProps
和getSnapshotBeforeUpdate
均是返回一個處理後的物件給componentDidUpdate
,所有需要操作的邏輯都放在componentDidUpdate
裡面。
原則上,getDerivedStateFromProps
+componentDidUpdate
可以替代componentWillReceiveProps
的所有正常功能,getSnapshotBeforeUpdate
+componentDidUpdate
可以替代componentDidMount
的所有功能。
具體的原因 和遷移指南 可以參考 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 的 state 和 生命週期 上開了 Hook(鉤子),React Hooks 提供了一些內建的 Hook 以便在合適的時機去使用它,同時組合內建的 Hook + 自己的業務邏輯 就可以生成新的 Custom Hook,同樣也可以掛載在 React Function Component 上。
三、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。入參是 initState,返回一個 turple,第一值是 state,第二個值是改變 state 的函式。
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 } }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
useEffect
相當於 class Component 中componentDidMount
、componentDidUpdate
、componentWillUnmount
三個生命週期的大綜合,在元件掛載、更新、解除安裝的時候都會執行 effect 裡面的函式。
在一個 Function Component 裡,和 useState 一樣可以可以使用多次 useEffect,這樣在組織業務邏輯的時候,就可以按照業務邏輯去劃分程式碼片段了(而不是 Class Component 中只能按照生命週期去劃分程式碼片段)。
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 呼叫都有自己的隔離空間,能保證不同的呼叫之間互不干擾。
useFriendStatus
以use
開頭是 React Hooks 的約定,這樣的話方便標識他是一個 Hook,同時 eslint 外掛也會去識別這種寫法,以防產生不必要的麻煩。
4.4 Context Hook
function Example() { const locale = useContext(LocaleContext); const theme = useContext(ThemeContext); // ... }
4.5 Reduce Hook
function Todos() { const [todos, dispatch] = useReducer(todosReducer); // ...
五、例子對比
該例子是 Dan 在 React Conf 上的例子,算是非常有代表性的了:
六、引入的問題
6.3 奇怪的 useEffect
6.2 底層實現導致邏輯上的問題
React Hook 在內部實現上是使用 xxx,因為使用 React Hook 有兩個限制條件
- 只能在頂層呼叫 Hooks,不能在迴圈、判斷條件、巢狀的函式裡面呼叫 Hooks
- 只允許 Function Hooks 和 Custom Hooks 呼叫 React Hook,普通函式不允許呼叫 Hooks
React Team 為此增加了 eslint 外掛:eslint-plugin-react-hooks ,算是變通的去解決問題吧。
七、常見疑問
為什麼 useState 返回的是個 tuple
效能問題
能標識所有的生命週期麼?
八、參考資料
- RFC: React Hooks
- Hooks at a Glance
- React Today and Tomorrow and 90% Cleaner React With Hooks
- React v16.7 "Hooks" - What to Expect
- Mixins Considered Harmful
- Why should I ever use a React functional component?
- This is why we need to bind event handlers in Class Components in React
- @babel/plugin-proposal-class-properties
- Update on Async Rendering
- Problematic React Lifecycle Methods are Going Away in React 17
- react hooks codesandbox