區(qū)別:1、hooks的寫法比class簡潔;2、hooks的業(yè)務代碼比class更加聚合;3、class組件的邏輯復用通常用render props以及HOC兩種方式,而react hooks提供了自定義hooks來復用邏輯。
本教程操作環(huán)境:Windows7系統(tǒng)、react17.0.1版、Dell G3電腦。
react hooks與class組件有哪些區(qū)別?下面就來帶大家對比一下react hooks和class組件,聊聊它們的區(qū)別。
react-hooks解決的問題
-
函數組件中不能擁有自己的狀態(tài)(state)。在hooks之前函數組件是無狀態(tài)的,都是通過props來獲取父組件的狀態(tài),但是hooks提供了useState來維護函數組件內部的狀態(tài)。
-
函數組件中不能監(jiān)聽組件的生命周期。useEffect聚合了多個生命周期函數。
-
class組件中生命周期較為復雜(在15版本到16版本的變化大)。
-
class組件邏輯難以復用(HOC,render props)。
hooks對比class的好處(對比)
1、寫法更加的簡潔
我們以最簡單的計數器為例:
class組件
class ExampleOfClass extends Component { constructor(props) { super(props) this.state = { count: 1 } } handleClick = () => { let { count } = this.state this.setState({ count: count+1 }) } render() { const { count } = this.state return ( <div> <p>you click { count }</p> <button onClick={this.handleClick}>點擊</button> </div> ) } }
hooks
function ExampleOfHooks() { const [count, setCount] = useState(0) const handleClick = () => { setCount(count + 1) } return ( <div> <p>you click { count }</p> <button onClick={handleClick}>點擊</button> </div> ) }
可以看到使用hooks的代碼相比class組件代碼更加的簡潔、清晰。
2、業(yè)務代碼更加聚合
使用class組件經常會出現一個功能出現在兩個生命周期函數內的情況,這樣分開寫有時候可能會忘記。比如:
let timer = null componentDidMount() { timer = setInterval(() => { // ... }, 1000) } // ... componentWillUnmount() { if (timer) clearInterval(timer) }
由于添加定時器和清除定時器是在兩個不同的生命周期函數,中間可能會有很多其他的業(yè)務代碼,所以可能會忘記清除定時器,如果在組件卸載時沒有添加清楚定時器的函數就可能會造成內存泄漏、網絡一直請求等問題。
但是使用hooks可以讓代碼更加的集中,方便我們管理,也不容易忘記:
useEffect(() => { let timer = setInterval(() => { // ... }, 1000) return () => { if (timer) clearInterval(timer) } }, [//...])
3、邏輯復用方便
class組件的邏輯復用通常用render props以及HOC兩種方式。react hooks提供了自定義hooks來復用邏輯。
下面以獲取鼠標在頁面的位置的邏輯復用為例:
class組件render props方式復用
import React, { Component } from 'react' class MousePosition extends Component { constructor(props) { super(props) this.state = { x: 0, y: 0 } } handleMouseMove = (e) => { const { clientX, clientY } = e this.setState({ x: clientX, y: clientY }) } componentDidMount() { document.addEventListener('mousemove', this.handleMouseMove) } componentWillUnmount() { document.removeEventListener('mousemove', this.handleMouseMove) } render() { const { children } = this.props const { x, y } = this.state return( <div> { children({x, y}) } </div> ) } } // 使用 class Index extends Component { constructor(props) { super(props) } render() { return ( <MousePosition> { ({x, y}) => { return ( <div> <p>x:{x}, y: {y}</p> </div> ) } } </MousePosition> ) } } export default Index
自定義hooks方式復用
import React, { useEffect, useState } from 'react' function usePosition() { const [x, setX] = useState(0) const [y, setY] = useState(0) const handleMouseMove = (e) => { const { clientX, clientY } = e setX(clientX) setY(clientY) } useEffect(() => { document.addEventListener('mousemove', handleMouseMove) return () => { document.removeEventListener('mousemove', handleMouseMove) } }) return [ {x, y} ] } // 使用 function Index() { const [position] = usePosition() return( <div> <p>x:{position.x},y:{position.y}</p> </div> ) } export default Index
可以很明顯的看出使用hooks對邏輯復用更加的方便,使用的時候邏輯也更加清晰。
hooks常見的一些API使用
1、useState
語法
const [value, setValue] = useState(0)
這種語法方式是ES6的數組結構,數組的第一個值是聲明的狀態(tài),第二個值是狀態(tài)的改變函數。
每一幀都有獨立的狀態(tài)
個人理解針對每一幀獨立的狀態(tài)是采用了閉包的方法來實現的。
function Example() { const [val, setVal] = useState(0) const timeoutFn = () => { setTimeout(() => { // 取得的值是點擊按鈕的狀態(tài),不是最新的狀態(tài) console.log(val) }, 1000) } return ( <> <p>{val}</p> <button onClick={()=>setVal(val+1)}>+</button> <button onClick={timeoutFn}>alertNumber</button> </> ) }
當組件的狀態(tài)或者props更新時,該函數組件會被重新調用渲染,并且每一次的渲染都是獨立的都有自己獨立的props以及state,不會影響其他的渲染。
2、useEffect
語法
useEffect(() => { //handler function... return () => { // clean side effect } }, [//dep...])
useEffect接收一個回調函數以及依賴項,當依賴項發(fā)生變化時才會執(zhí)行里面的回調函數。useEffect類似于class組件didMount、didUpdate、willUnmount的生命周期函數。
注意點
-
useEffect是異步的在組件渲染完成后才會執(zhí)行
-
useEffect的回調函數只能返回一個清除副作用的處理函數或者不返回
-
如果useEffect傳入的依賴項是空數組那么useEffect內部的函數只會執(zhí)行一次
3、useMemo、useCallback
useMemo和useCallback主要用于減少組件的更新次數、優(yōu)化組件性能的。
-
useMemo接收一個回調函數以及依賴項,只有依賴項變化時才會重新執(zhí)行回調函數。
-
useCallback接收一個回調函數以及依賴項,并且返回該回調函數的memorize版本,只有在依賴項重新變化時才會重新新的memorize版本。
語法
const memoDate = useMemo(() => data, [//dep...]) const memoCb = useCallback(() => {//...}, [//dep...])
在優(yōu)化組件性能時針對class組件我們一般使用React.PureComponent,PureComponent會在shouldUpdate進行一次錢比較,判斷是否需要更新;針對函數組件我們一般使用React.memo。但是在使用react hooks時由于每一次渲染更新都是獨立的(生成了新的狀態(tài)),即使使用了React.memo,也還是會重新渲染。
比如下面這種場景,改變子組件的name值后由于父組件更新后每次都會生成新值(addAge函數會改變),所以子組件也會重新渲染。
function Parent() { const [name, setName] = useState('cc') const [age, setAge] = useState(22) const addAge = () => { setAge(age + 1) } return ( <> <p>父組件</p> <input value={name} onChange={(e) => setName(e.target.value)} /> <p>age: {age}</p> <p>-------------------------</p> <Child addAge={addAge} /> </> ) } const Child = memo((props) => { const { addAge } = props console.log('child component update') return ( <> <p>子組件</p> <button onClick={addAge}>click</button> </> ) })
使用useCallback優(yōu)化
function Parent() { const [name, setName] = useState('cc') const [age, setAge] = useState(22) const addAge = useCallback(() => { setAge(age + 1) }, [age]) return ( <> <p>父組件</p> <input value={name} onChange={(e) => setName(e.target.value)} /> <p>age: {age}</p> <p>-------------------------</p> <Child addAge={addAge} /> </> ) } const Child = memo((props) => { const { addAge } = props console.log('child component update') return ( <> <p>子組件</p> <button onClick={addAge}>click</button> </> ) })
只有useCallback的依賴性發(fā)生變化時,才會重新生成memorize函數。所以當改變name的狀態(tài)是addAge不會變化。
4、useRef
useRef類似于react.createRef。
const node = useRef(initRef)
useRef 返回一個可變的 ref 對象,其 current 屬性被初始化為傳入的參數(initRef)
作用在DOM上
const node = useRef(null) <input ref={node} />
這樣可以通過node.current屬性訪問到該DOM元素。
需要注意的是useRef創(chuàng)建的對象在組件的整個生命周期內保持不變,也就是說每次重新渲染函數組件時,返回的ref 對象都是同一個(使用 React.createRef ,每次重新渲染組件都會重新創(chuàng)建 ref)。
5、useReducer
useReducer類似于redux中的reducer。
語法
const [state, dispatch] = useReducer(reducer, initstate)
useReducer傳入一個計算函數和初始化state,類似于redux。通過返回的state我們可以訪問狀態(tài),通過dispatch可以對狀態(tài)作修改。
const initstate = 0; function reducer(state, action) { switch (action.type) { case 'increment': return {number: state.number + 1}; case 'decrement': return {number: state.number - 1}; default: throw new Error(); } } function Counter(){ const [state, dispatch] = useReducer(reducer, initstate); return ( <> Count: {state.number} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ) }
6、useContext
通過useContext我們可以更加方便的獲取上層組件提供的context。
父組件
import React, { createContext, Children } from 'react' import Child from './child' export const MyContext = createContext() export default function Parent() { return ( <div> <p>Parent</p> <MyContext.Provider value={{name: 'cc', age: 21}}> <Child /> </MyContext.Provider> </div> ) }
子組件
import React, { useContext } from 'react' import { MyContext } from './parent' export default function Parent() { const data = useContext(MyContext) // 獲取父組件提供的context console.log(data) return ( <div> <p>Child</p> </div> ) }
使用步驟
- 父組件創(chuàng)建并導出
context:export const MyContext = createContext()
- 父組件使用
provider
和value
提供值:<MyContext.provide value={{name: 'cc', age: 22}} />
- 子組件導入父組件的
context:import { MyContext } from './parent'
- 獲取父組件提供的值:
const data = useContext(MyContext)
不過在多數情況下我們都不建議使用context
,因為會增加組件的耦合性。
7、useLayoutEffect
useEffect 在全部渲染完畢后才會執(zhí)行;useLayoutEffect 會在 瀏覽器 layout之后,painting之前執(zhí)行,并且會柱塞DOM;可以使用它來讀取 DOM 布局并同步觸發(fā)重渲染。
export default function LayoutEffect() { const [color, setColor] = useState('red') useLayoutEffect(() => { alert(color) // 會阻塞DOM的渲染 }); useEffect(() => { alert(color) // 不會阻塞 }) return ( <> <div id="myDiv" style={{ background: color }}>顏色</div> <button onClick={() => setColor('red')}>紅</button> <button onClick={() => setColor('yellow')}>黃</button> </> ) }
上面的例子中useLayoutEffect會在painting之前執(zhí)行,useEffect在painting之后執(zhí)行。
hooks讓函數組件擁有了內部狀態(tài)、生命周期,使用hooks讓代碼更加的簡介,自定義hooks方便了對邏輯的復用,并且擺脫了class組件的this問題;但是在使用hooks時會產生一些閉包問題,需要仔細使用。
【