本篇為 [FE302] React 基礎 - hooks 版本 這門課程的學習筆記。如有錯誤歡迎指正!
參考:從實際案例看 class 與 function component 的差異
在學會如何在 React 中,以 Function component 搭配 Hooks 寫出簡單的 Todo List 之後,再來要探討使用 Function 或 Class 寫 component 的差異,即使目前主流是使用 Function component,未來還是有機會碰到 Class component 的寫法。
Function component vs Class component 在 React 16.8 之前,因為 function component 還沒有 useState、Hooks 的概念,需要描述 component 的狀態時通常會使用 Class component。
但在 React 16.8 有了 Hooks 以後,就能夠在 Function component 引入 Hooks 來表示狀態,這種寫法也成為目前主流。
而 class component 與 function component 兩者之間的差別主要在於:
class component:關注的是這個「生命週期」要做什麼,
function component:每一次 render,都是「重新」呼叫一次 function,並且會記住「當下」傳入的值
什麼是 Class component? 顧名思義,就是用 class 去實作一個 component,但這種寫法比起 function component,其實需要具備 JavsScript 物件導向的相關知識。
舉例來說,在之前 Todo List 以 function 寫一個 Button component:
1 2 3 4 5 6 7 8 9 10 function Button ({ onClick, children } ) { return <button onClick ={onClick} > {children}</button > ; } function App ( ) { return ( <div className ="App" > <Button onClick ={handleButtonClick} > Add Todo</Button > // 以下略 }
換成 class component 的寫法如下,兩者的功能其實相同:
1 2 3 4 5 6 7 8 9 10 import React from "react" ;class Button extends React.Component { render ( ) { const { onClick, children } = this .props ; return <button onClick ={onClick} > {children}</button > ; } }
範例:改寫 TodoItem component 或是改寫之前用 function 寫的 TodoItem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 export default function TodoItem ({ todo, handleDeleteTodo, handleToggleIsDone, } ) { const handleToggleClick = ( ) => { handleToggleIsDone (todo.id ); }; const handleDeleteClick = ( ) => { handleDeleteTodo (todo.id ); }; return ( <TodoItemWrapper data-todo-id ={todo.id} > <TodoContent $isDone ={todo.isDone} > {todo.content}</TodoContent > <TodoButtonWrapper > <Button onClick ={handleToggleClick} > {todo.isDone && "已完成"} {!todo.isDone && "未完成"} </Button > <RedButton onClick ={handleDeleteClick} > 刪除</RedButton > </TodoButtonWrapper > </TodoItemWrapper > ); }
以 class component 改寫如下,但這樣寫其實會出現錯誤訊息,this 的值會是 undefined:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 export default class TodoItemC extends React.Component { handleToggleClick ( ) { const { handleToggleIsDone, todo } = this .props ; handleToggleIsDone (todo.id ); } handleDeleteClick ( ) { const { handleDeleteTodo, todo } = this .props ; handleDeleteTodo (todo.id ); } render ( ) { const { todo } = this .props ; return ( <TodoItemWrapper data-todo-id ={todo.id} > <TodoContent $isDone ={todo.isDone} > {todo.content}</TodoContent > <TodoButtonWrapper > <Button onClick ={handleToggleClick} > {todo.isDone ? "已完成" : "未完成"} </Button > <RedButton onClick ={handleDeleteClick} > 刪除</RedButton > </TodoButtonWrapper > </TodoItemWrapper > ); } }
這是因為 this 的值會根據怎麼呼叫 function 決定,在嚴格模式中直接呼叫 onClick 的話 this 的值就會是 undefined:
有兩種解決方式:
透過 cunstructor 初始化 props 並綁定 this 指向
改成 classmethod 綁定 this 指向
透過 cunstructor 初始化 props 並綁定 this 指向 透過 constructor,將 props 初始化,在利用 bind 來綁定 this 指向 constructor 裡面的 this,也就是 TodoItemC 這個 component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 export default class TodoItemC extends React.Component { constructor (props ) { super (props); this .handleToggleClick = this .handleToggleClick .bind (this ); this .handleDeleteClick = this .handleDeleteClick .bind (this ); } handleToggleClick ( ) { const { handleToggleIsDone, todo } = this .props ; handleToggleIsDone (todo.id ); } handleDeleteClick ( ) { const { handleDeleteTodo, todo } = this .props ; handleDeleteTodo (todo.id ); } render ( ) { const { todo } = this .props ; return ( <TodoItemWrapper data-todo-id ={todo.id} > <TodoContent $isDone ={todo.isDone} > {todo.content}</TodoContent > <TodoButtonWrapper > // 這裡要加上 this 使用 <Button onClick ={this.handleToggleClick} > {todo.isDone ? "已完成" : "未完成"} </Button > <RedButton onClick ={this.handleDeleteClick} > 刪除</RedButton > </TodoButtonWrapper > </TodoItemWrapper > ); } }
改用 classmethod 綁定 this 指向 另一種解決方法,就是改用 classmethod 寫法,類似箭頭函式,同樣能綁定 this:
1 2 3 4 5 6 7 8 9 10 11 export default class TodoItemC extends React.Component { handleToggleClick = () => { const { handleToggleIsDone, todo } = this .props ; handleToggleIsDone (todo.id ); }; handleDeleteClick = () => { const { handleDeleteTodo, todo } = this .props ; handleDeleteTodo (todo.id ); };
Class component 中的 state 在 Class Component 的 state 同樣要寫在 constructor 裡面,進行 props 初始化,以及設定初始 state:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export default class TodoItemC extends React.Component { constructor (props ) { super (props); this .state = { counter : 1 , }; } handleToggleClick = () => { const { handleToggleIsDone, todo } = this .props ; handleToggleIsDone (todo.id ); this .setState = { counter : this .state .counter + 1 , }; };
Class component 的生命週期 關於 class component 的生命週期架構可參考附圖:
(圖片來源:React LifeCycle Methods Diagram )
可和之前提過的 React Hook 流程圖進行對照,改成用 useEffect 執行:
(圖片來源:https://github.com/donavon/hook-flow)
實作一個 Counter component 這裡重新建立一個 Counter.js 作為範例,首先將 index.js 改成引入 Counter:
1 2 3 4 5 import React from "react" ;import ReactDOM from "react-dom" ;import Counter from "./Counter" ;ReactDOM .render (<Counter /> , document .getElementById ("root" ));
建立 Counter.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import React from "react" ;export default class Counter extends React.Component { constructor (props ) { super (props); this .state = { counter : 1 , }; } handleClick = () => { this .setState ({ counter : this .state .counter + 1 , }); }; render ( ) { const { counter } = this .state ; return ( <div > <button onClick ={this.handleClick} > +1</button > counter: {counter} </div > ); } }
結果如下,藉由點擊事件來改變 component 狀態:
Test component & 內建 method 加上 Test component,並設定只有在 count 等於 1 時會出現 Test,以及使用 React 內建 method 來觀察 component 的生命週期:
componentDidMount:會在 component mount 之後執行
componentDidUpdate:會在 component update 之後執行
componentWillUnmount:會在 component unmount 之前執行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 class Test extends React.Component { componentDidMount() { console.log("test mount"); } componentWillUnmount() { console.log("test unmount"); } render() { return <div>test!</div>; } } export default class Counter extends React.Component { constructor(props) { super(props); this.state = { counter: 1, }; console.log("constructor"); } // 使用 react 內建的 method, this 會指向這個 component componentDidMount() { // 會在 component mount 之後執行 console.log("did mount", this.state); } // 拿到上一次的參數: prevProps 和 prevState componentDidUpdate(prevProps, prevState) { // 會在 component update 之後執行 console.log("prevState", prevState); console.log("update!"); } componentWillUnmount() { // 會在 component unmount 之前執行 console.log("unmount"); } handleClick = () => { this.setState({ counter: this.state.counter + 1, }); }; render() { const { counter } = this.state; console.log("render"); return ( <div> <button onClick={this.handleClick}>+1</button> counter: {counter} {counter === 1 && <Test />} </div> ); } }
結果如下:
第一次渲染畫面,只有第一次會有 constructor 和 mount:
點擊第一次,第二次渲染畫面,count 不等於 1,test unmount:
點擊第二次,第三次渲染畫面:
其他少見的 method 輸入 component 會發現有些 method 被畫刪除線,代表目前版本不建議使用:
componentDidCatch:進行錯誤處理
shouldComponentUpdate:決定要不要 update,也可透過傳入的參數決定要不要 update,詳細可參考官方文件
舉例來說,在 Counter component 加入這段,若 return false 就不會進行 update;反之 return true 就會:
1 2 3 shouldComponentUpdate (nextProps, nextState ) { return false ; }
以下舉個簡單範例:
1 2 3 4 5 shouldComponentUpdate (nextProps, nextState ) { if (nextState.counter > 5 ) return false ; return true ; }
結果如下,當 counter: 5 之後,再點擊也不會有反應:
這通常會和之前在 React 效能優化提到的 memo 搭配使用,根據比對 props 是否相同或自訂條件。
另一個方法,是把 Component 改寫成 PureComponent,和 memo 的效果類似:
1 export default class Counter extends React.PureComponent
React 會自動進行優化,加上 shouldComponentUpdate 判斷,當 props 裡面的屬性有變動時才會進行 update,沒有的話就不進行 re-render。
–
結語 在實作 React 時,會瞭解到 class component 和 function component 用不同方式去思考如何建立 component,背後的概念其實差蠻多的,需要轉變成另一種想法。
最後再簡單記錄 class component 和 function component 兩者之間的差異:
class component
透過 ES6 語法來實作物件導向的 class component
由於 this 指向的關係,state 和 props 會拿到最新的結果,但是會較不易於進行 callback 操作
提供許多 lifecycle method 使用,方便管理較複雜的 component 狀態
function component
透過閉包的形式來管理狀態的 function component
把許多 method 都寫在 function 中,自己本身就像是 render function,較容易抽出共同邏輯,或是進行模組化測試
生命週期的方法,是以 useEffect 來決定 render 要做的事情
參考文章: