React學(xué)習(xí):理解和使用Hooks

Hooks 是React的一次革命性升級,本文將對其優(yōu)勢和API進(jìn)行比較全面的解析

為什么要有hooks

在沒有hooks之前,除了對于一些無狀態(tài)組件可以使用函數(shù)來聲明組件以外,大家都會使用class來聲明組件。作為一個主要工作內(nèi)容為Android開發(fā)的我,早已習(xí)慣萬物皆class,而Android中的Activity(可以理解為每一個交互界面)就是class,所以也欣然接受,并且在ES6中class帶有的constructor、super以及react的生命周期函數(shù)對使用Java的Android開發(fā)者來講很容易理解接受。

但是使用class來聲明組件卻存在以下問題:

  1. 狀態(tài)邏輯難以復(fù)用
    假如有一段邏輯代碼需要在多個組件中使用,那么在以前,可以通過以下幾個方式來實(shí)現(xiàn):
  • copy代碼,顯然不符合代碼設(shè)計的原則
  • 繼承,一方面,js只支持單繼承(Java中可以通過接口實(shí)現(xiàn)對多個類的實(shí)現(xiàn)),想要對復(fù)用多個組件的邏輯就無能為力;另一方面,只為了復(fù)用部- 分邏輯而濫用繼承,顯然是違背oop原則的
  • HOC高階組件,使用HOC復(fù)用的原理很簡單,就是包裹封裝,比如我們想實(shí)現(xiàn)對onScroll方法的復(fù)用:
import React,{Component} from 'react'

function scrollable(Child) {
    return class ScrollWrapper extends Component {
        ref = React.createRef()
        onScroll = (...args) => {
            console.log('onscroll')
            this.ref.current.onScroll(...args)
        }
        componentDidMount() {
            document.addEventListener('scroll', this.onScroll, false);
        }

        componentWillUnmount() {
            document.removeEventListener('scroll', this.onScroll, false);
        }

        render() {
            return <Child ref={this.ref} />
        }
    }
}

class ScrollableApp extends Component {
    onScroll(){
        console.log('child onscroll')
    }
    render() { 
        return <div style={{color:'red',height:10000,width:800}}>
            
        </div>
    }
}

export default scrollable(ScrollableApp);

ScrollableApp通過高階組件即函數(shù)scrollable()封裝后,復(fù)用了ScrollWrapper中的onScroll()方法,并且還能再ScrollWrapper的onScroll使子組件ScrollableApp中的onScroll也能得到調(diào)用;使用渲染屬性也可以實(shí)現(xiàn)復(fù)用:

export class Scrollable extends Component {
    onScroll = (...args) => {
        console.log('onscroll')
        console.log(this.props.children)        
    }
    componentDidMount() {
        document.addEventListener('scroll', this.onScroll, false);
    }

    componentWillUnmount() {
        document.removeEventListener('scroll', this.onScroll, false);
    }

    render() {
        return this.props.render()
    }
}

export class ScrollableApp extends Component {
    onScroll() {
        console.log('child onscroll')
    }
    render() {
        return <div style={{ color: 'red', height: 10000, width: 800 }}>

        </div>
    }
}

使用時:

function App() {
  return (
    <div>
      <Scrollable render={() => <ScrollableApp></ScrollableApp>}></Scrollable>
    </div>
  );
}

export default App;

這個方法和高階組件方式差不多,不再贅述
使用這兩者雖然能實(shí)現(xiàn)邏輯復(fù)用,但無疑對代碼的簡潔和運(yùn)行性能都有不少的損耗

  • 另外,“組合優(yōu)于繼承”,或許可以使用策略模式,抽取出不同的類來封裝這些邏輯,在使用時引入相關(guān)的類來實(shí)現(xiàn),但這無疑有點(diǎn)過度設(shè)計,也大大增加了代碼結(jié)構(gòu)的復(fù)雜度
  1. 類組件復(fù)雜,難以維護(hù),主要指生命周期函數(shù)混亂,比如上面的onScroll監(jiān)聽,在componentDidMount和componentWillUnmount分別要注冊反注冊,相關(guān)的邏輯分散在不同地方,而在componentDidMount往往還需要處理類似網(wǎng)絡(luò)請求等各種初始化的動作,也導(dǎo)致不相關(guān)的邏輯混雜在一起,使得代碼難以維護(hù)(這個在Android開發(fā)中其實(shí)也是再正常不過做法...)

  2. this指向等問題
    上面有一段代碼:

onScroll = (...args) => {
   ...
}

這里使用類屬性的方式定義onScroll,才能通過this.onScroll訪問到該方法,而如果聲明為類成員函數(shù),則在向下一級組件傳遞回調(diào)函數(shù)時無法正確訪問到該方法

而hooks則很好的解決了以上的問題

使用hooks

useState

  1. 使用

在沒有react hooks之前,組件可以分為有狀態(tài)組件和無狀態(tài)組件,如:

class Counter extends Component {
    state = {
        count: 0
    }
    render() {
        return (
            <div>
                {this.state.count}
            </div>
        )
    }
}
function Counter(props) {
    return (
        <div>
            {props.count}
        </div>
    )
}

第一種寫法Counter中存儲了狀態(tài)state,而函數(shù)組件寫法中只能通過props來獲取狀態(tài)

使用useState:

import React, { useState } from 'react'

export default function Counter(props) {
    const [count, setCount] = useState(0)
    return (
        <div>
            <button
                onClick={() => {
                    setCount(count=>count + 1)
                }}>
            </button>
            {count}
        </div>
    )
}

這里const [count, setCount] = useState(0)中,相當(dāng)于定義了一個count變量作為該組件state的一個屬性,而useState中傳入的值為count的初始默認(rèn)值(也可以不傳入,則為undefined),setCount為改變count值的方法;以上代碼等價于:

export class Counter extends React.Component {
    state = {
        count: 0
    }
    render() {
        return (
            <div>
                <button
                    onClick={() => {
                        this.setState(
                            {
                                count: this.state.count + 1
                            }
                        )
                    }}>
                </button>
                {this.state.count}
            </div>
        )
    }
}

可見,使用useState使得代碼大大簡化,可以不使用class聲明組件,也不用擔(dān)心this指向的問題;并且從此我們不用再以有無狀態(tài)來區(qū)分組件了,因為函數(shù)組件也可以擁有狀態(tài)

2.原理
這里有幾個值得探討的問題:

  • useState()如何確定應(yīng)該返回的是哪一個component的state
    這個很簡單,因為js運(yùn)行在單線程環(huán)境中,所以在運(yùn)行到某一個useState函數(shù)時,可以獲取到對應(yīng)的運(yùn)行上下文處在哪一個component中

  • 如何確定useState對應(yīng)于哪一個返回值
    思考以下的偽代碼:

function Counter(props) {
  if (someCondition) {
    useState();
  }
  useState();
}

在實(shí)際執(zhí)行中會報錯,而且如果eslint配置了react-hooks/rules-of-hooks,會直接編譯報錯
實(shí)際上為了代碼盡可能簡潔,useState是通過記錄第一次運(yùn)行時的順序來確定之后的每次運(yùn)行分別返回對應(yīng)哪個state的,所以Hooks函數(shù)必須始終以相同的次序和數(shù)量被調(diào)用

  • setState相同值的時候會否重新渲染
    改寫之前的代碼,setCount時每次都為0,發(fā)現(xiàn)并不會執(zhí)行render函數(shù)
function Counter(props) {
    const [count, setCount] = useState(0)
    console.log('render')
    return (
        <div>
            <button
                onClick={() => {
                    setCount(0)
                }}>
            </button>
            {count}
        </div>
    )
}

假如我們的state中存儲的是對象呢?

function Counter(props) {
    const [countObj, setCountObj] = useState({ count: 0 })
    console.log('render')
    return (
        <div>
            <button
                onClick={() => {
                    countObj.count = countObj.count + 1
                    setCountObj(countObj)
                }}>
            </button>
            {countObj.count}
        </div>
    )
}

點(diǎn)擊button,發(fā)現(xiàn)也未重新渲染。因此在setState時,如果為對象,對比的地址值未改變,并不會重新render,這和PureComponent類似

useEffect

effec被翻譯過來為副作用,但是這個確很容易產(chǎn)生語義誤解;實(shí)際上副作用實(shí)際上指的是視圖組件與視圖組件之外系統(tǒng)進(jìn)行交互的行為u,比如與DOM交互,網(wǎng)絡(luò)請求,數(shù)據(jù)持久化操作等
假如我們需要在componentDidMount之后設(shè)置onScroll監(jiān)聽,使用class的寫法為:

class Scrollable extends React.Component {
    onScroll = () => {
        console.log('onscroll')
    }
    componentDidMount() {
        document.addEventListener('scroll', this.onScroll, false);
    }
}

通過useEffect改寫則為:

function Scrollable(props) {
    useEffect(() => {
        document.addEventListener('scroll', this.onScroll, false);
    })
    return <div></div>
}

useEffect()中的函數(shù)會在每次componentDidMount、componentDidUpdate的時候執(zhí)行,如果要在componentWillUnmount中取消監(jiān)聽也很簡單,只需在useEffect()傳入的函數(shù)中return相關(guān)處理函數(shù)即可:

function Scrollable(props) {
    useEffect(() => {
        document.addEventListener('scroll', this.onScroll, false);
        return () => {
            document.removeEventListener('scroll', this.onScroll, false);
        };
    })
    return <div></div>
}

除此之外,useEffect還可以傳入第二個參數(shù),該參數(shù)類型為數(shù)組;這里分為三種情況:

1.不傳入數(shù)組參數(shù):在不傳入該數(shù)組的情況下(參考上面的代碼),每次componentDidMount、componentDidUpdate或componentWillUnmount時都會執(zhí)行對應(yīng)的副作用函數(shù)

2.傳入空數(shù)組:該副作用會在組件整個生命周期中只執(zhí)行一次、清理一次;這很適用于對網(wǎng)絡(luò)請求、事件監(jiān)聽等操作

3.傳入非空數(shù)組:該副作用會在數(shù)組中的各個參數(shù)發(fā)生變化時(對象比較地址值),才會在對應(yīng)的生命周期中重新執(zhí)行

通過useEffect,可以使得我們的代碼更簡潔,邏輯更清晰易維護(hù),對數(shù)組參數(shù)的控制也能幫助我們更輕松的寫出高性能的代碼

useContext

在沒有hooks之前Context就已經(jīng)存在,用以實(shí)現(xiàn)跨層級數(shù)據(jù)傳遞,一般使用Consumer和ContextType實(shí)現(xiàn)

但是Context的似乎使用得不是很多,多數(shù)還是通過redux的store存儲全局?jǐn)?shù)據(jù);useContext使得Context的使用更容易,可以在函數(shù)組件中使用Context,并且不用依賴ContextType,避免了每一個組件只能對應(yīng)一個ContextType的缺點(diǎn),當(dāng)然也不需要Consumer

使用很簡單(這里只是演示,代碼結(jié)構(gòu)可以根據(jù)實(shí)際做優(yōu)化;這里順便列出以往使用Consumer和ContextType實(shí)現(xiàn)Context數(shù)據(jù)傳遞的寫法作對比):

import React, { Component, createContext, useContext, useState } from 'react'
const CountContext = createContext(0)

function App() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <button
        onClick={() => {
          setCount(count + 1)
        }}>
      </button>
      <CountContext.Provider value={count}>
        <CounterByConsummer></CounterByConsummer>
        <Counter></Counter>
        <CounterByContextType></CounterByContextType>
      </CountContext.Provider>
    </div>
  );
}

//useContext寫法
function Counter(props) {
  const count = useContext(CountContext)
  return (
    <div>
      {count}
    </div>
  )
}
//Consummer寫法
class CounterByConsummer extends Component {
  render() {
    return (
      <CountContext.Consumer>
        {count => <div>{count}</div>}
      </CountContext.Consumer>
    )
  }
}

//ContextType寫法
class CounterByContextType extends Component {
  static contextType = CountContext
  render() {
    const count = this.context
    return (
      <div>
        {count}
      </div>
    )
  }
}

export default App;

需要注意的一點(diǎn)是,不要濫用context,因為會破壞組件的獨(dú)立性

useMemo&useCallback

理解memo

為了提高react的運(yùn)行效率,避免無用的重渲染,我們常使用繼承PureComponent的方式;而在函數(shù)組件則可以使用React.memo(Component)來達(dá)到同樣的效果;

使用useMemo

React.memo()針對組件,而useMemo則是針對組件的方法,思考如下代碼:

import React, { useMemo, useState } from 'react'

function App() {
  const [name, setName] = useState('smartzheng')
  const [age, setAge] = useState(18)
  return (
    <>
      <button
        onClick={() => {
          setName(name + 'changed')
        }}>
        changeName
      </button>
      <button
        onClick={() => {
          setAge(age + 1)
        }}>
        changeAge
      </button>
      <Description age={age} name={name}></Description>
    </>
  );
}


function Description({ name, age }) {
  function getAge(age) {
    console.log('changeAge')
    return age + '歲'
  }
  const newAge = useMemo(() => getAge(age), [age])

  return <>
    <div>
      {name}
    </div>
    <div>
      {newAge}
    </div></>
}
export default App;

這段代碼主要做的是顯示兩個button,一個用來改變name,一個改變age,而子組件中對name和age進(jìn)行顯示,并且通過getAge()在age后加上“歲”字。測試發(fā)現(xiàn),點(diǎn)擊changeName和changeAge都會導(dǎo)致子組件重新執(zhí)行g(shù)etAge(),這并不是我們想要的結(jié)果;這里就可以通過useMemo來實(shí)現(xiàn)只有在age發(fā)生變化時才執(zhí)行g(shù)etAge(),使用很簡單,只需將 const newAge = useMemo(() => getAge(age), [age]) 改為const newAge = useMemo(() => getAge(age), [age])即可

useMemo(() => getAge(age), [age])中,傳入的是一個函數(shù),可以理解為一個回調(diào),數(shù)組[age]代表該回調(diào)只有在age變化時才會執(zhí)行,而執(zhí)行的內(nèi)容為getAge(age)

使用useCallback

useCallback和useMemo很類似,不過他返回的是緩存的函數(shù):const fnA = useCallback(fnB, [a]),代表useCallback會將fnB函數(shù)返回,返回值是否改變依賴于a值是否改變。舉例如下:

import React, { useState, useCallback } from 'react';
const set = new Set();

export default function Callback() {
  const [count, setCount] = useState(0);
  const [val, setVal] = useState('');

  const callback = useCallback(() => {
    console.log(count);
  }, [count]);
  set.add(callback);
  return <div>
    <h1>{count}</h1>
    <h1>{set.size}</h1>
    <div>
      <button onClick={() => setCount(count + 1)}>changeCount</button>
      <input value={val} onChange={event => setVal(event.target.value)} />
    </div>
  </div>;
}

這里的callback就是對() => {console.log(count)的緩存,通過一個set來存放它,只有當(dāng)點(diǎn)擊changeCount的按鈕是set的size才會發(fā)生變化,說明只有在count變化時,才會返回新的callback方法,這對減少重復(fù)創(chuàng)建相同的方法對象很有幫助

useRef

在hooks之前,常用createRef來創(chuàng)建ref來獲取DOM元素的引用,React Hooks中則提供了useRef

  1. 使用useRef獲取DOM元素的引用
import React, { PureComponent, useRef } from 'react';

function App(props) {
  const countRef = useRef();
  return (
    <>
      <Counter ref={countRef}></Counter>
      <button onClick={() => { console.log(countRef.current) }}></button>
    </>
  )
}

class Counter extends PureComponent {
  render() {
    return (
      <div>

      </div>
    )
  }
}
export default App

在上述代碼中,通過useRef()創(chuàng)建了countRef并在Counter組件上進(jìn)行賦值,點(diǎn)擊button,每次都會正確打印出Counter組件
值得注意的是,這里的Counter如果使用函數(shù)組件則會報錯,提示function components can not be given refs,原因是函數(shù)組件會被React底層處理,被class wrap,所以直接給該函數(shù)組件進(jìn)行ref賦值沒有意義;從這里也可以發(fā)現(xiàn)函數(shù)組件還不能完全替代類組件

  1. 使用useRef存儲對象
    正常情況下,如果在函數(shù)組件中聲明一個變量,那么該變量會在每次渲染時重新創(chuàng)建,而使用useRef可以實(shí)現(xiàn)跨越聲明周期存儲數(shù)據(jù),思考如下代碼:
import React, { useRef,useEffect,useState } from 'react'

function App(props) {
  const [count, setCount] = useState(0)
  let interval;
  useEffect(()=>{
    interval = setInterval(()=>{
      setCount(count+1)
    },1000)
  },[])
  if(count>5){
    clearInterval(interval)
  }
  return (
    <>
      <div>{count}</div>
    </>
  )
}

export default App

當(dāng)count大于5時,清除定時器,這樣寫肯定是無效的,因為每次都會創(chuàng)建一個新的interval,clearInterval中的interval并不是最開始的interval,通過useRef改寫即可實(shí)現(xiàn):

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

function App(props) {
  const [count, setCount] = useState(0)
  let interval = useRef();
  useEffect(()=>{
    interval.current = setInterval(()=>{
      setCount(count+1)
    },1000)
  },[])
  if(count>5){
    clearInterval(interval.current)
  }
  return (
    <>
      <div>{count}</div>
    </>
  )
}

export default App

自定義Hooks

前面提到類組件有三個缺點(diǎn),首當(dāng)其沖的是邏輯復(fù)用問題,我們可以通過自定義Hooks來解決該問題,例如我們通過自定義useCount來復(fù)用一個定時器:

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

function App(props) {
  const [count] = useCount(0)
  return (
    <>
      <div>{count}</div>
    </>
  )
}

function useCount(defaultCount){
  const [count, setCount] = useState(0)
  let interval = useRef();
  useEffect(() => {
    interval.current = setInterval(() => {
      setCount(count => count + 1)
    }, 1000)
  }, [])
  useEffect(()=>{
    if (count >= 5) {
      clearInterval(interval.current)
    }
  })
  return [count, setCount]
}
export default App

關(guān)于React Hooks的優(yōu)勢和常用API使用先寫到這,后面的文章再對Hooks的深層實(shí)現(xiàn)原理和自定義Hooks學(xué)習(xí)和解析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容