嘔心瀝血,一文看懂 react hooks

介紹

react hooksReact 16.8 的新增特性。 它可讓咱們在函數組件中使用 state 、生命週期以及其餘 react 特性,而不只限於 class 組件javascript

react hooks 的出現,標示着 react 中不會在存在無狀態組件了,只有類組件和函數組件html

對比

存在即合理,hooks 也不例外,它的出現,就表明了它要解決一些 class 組件的缺陷或者不足,那麼咱們先來看看 class 組件有什麼不足或者問題存在前端

根據網上的說法我總結出三點,固然每種問題都有其解決方案java

問題 解決方案 缺點
生命週期臃腫、邏輯耦合
邏輯難以複用 經過繼承解決 不支持多繼承
經過hoc解決 會增長額外的組件嵌套,也會有一些性能影響
渲染屬性 同上、層級臃腫、性能影響
class this 指向問題 匿名函數 每次都建立新的函數,子組件重複沒必要要渲染
bind 須要寫不少跟邏輯、狀態無關的代碼

hooks 對這些問題都有較好的解決方案react

  1. 沒有了 class, 天然就沒有了 this 指向問題
  2. 經過自定義 useEffect 來解決複用問題
  3. 經過使用 useEffect 來細分邏輯,減少出現邏輯臃腫的場景

固然,hooks 是一把雙刃劍,用的好本身可以達到效果,用的很差反而會 下降開發效率和質量,那麼咱們接下來看看如用更好的使用 hooksgit

具體使用

useState 的使用

簡單例子

hooks 的能力,就是讓咱們在函數組件中使用 state, 就是經過 useState 來實現的,咱們來看一個簡單的例子es6

function App () {
    const [ count, setCount ] = useState(0)
    return (
      <div> 點擊次數: { count } <button onClick={() => { setCount(count + 1)}}>點我</button> </div>
      )
  }
複製代碼

useState 的使用很是簡單,咱們從 React 中拿到 useState 後,只須要在使用的地方直接調用 useState 函數就能夠, useState 會返回一個數組,第一個值是咱們的 state, 第二個值是一個函數,用來修改該 state 的,那麼這裏爲何叫 countsetCount?必定要叫這個嗎,這裏使用了 es6 的解構賦值,因此你能夠給它起任何名字,updateCount, doCountany thing,固然,爲了編碼規範,因此建議統一使用一種命名規範,尤爲是第二個值github

相同值

當咱們在使用 useState 時,修改值時傳入一樣的值,咱們的組件會從新渲染嗎,例如這樣web

function App () {
    const [ count, setCount ] = useState(0)
    console.log('component render count')
    return (
      <div> 點擊次數: { count } <button onClick={() => { setCount(count)}}>點我</button> </div>
      )
  }
複製代碼

結果是不會,放心使用redux

useState 的默認值

useState 支持咱們在調用的時候直接傳入一個值,來指定 state 的默認值,好比這樣 useState(0), useState({ a: 1 }), useState([ 1, 2 ]),還支持咱們傳入一個函數,來經過邏輯計算出默認值,好比這樣

function App (props) {
    const [ count, setCount ] = useState(() => {
      return props.count || 0
    })
    return (
      <div> 點擊次數: { count } <button onClick={() => { setCount(count + 1)}}>點我</button> </div>
      )
  }
複製代碼

這個時候,就有小夥伴問了,那我組件每渲染一次,useState 中的函數就會執行一邊嗎,浪費性能,其實不會,useState 中的函數只會執行一次,咱們能夠作個測試

function App (props) {
    const [ count, setCount ] = useState(() => {
      console.log('useState default value function is call')
      return props.count || 0
    })
    return (
      <div> 點擊次數: { count } <button onClick={() => { setCount(count + 1)}}>點我</button> </div>
      )
  }
複製代碼

結果是

setUseState 時獲取上一輪的值

咱們在使用 useState 的第二個參數時,咱們想要獲取上一輪該 state 的值的話,只須要在 useState 返回的第二個參數,也就是咱們上面的例子中的 setCount 使用時,傳入一個參數,該函數的參數就是上一輪的 state 的值

setCount((count => count + 1)

複製代碼

多個 useState 的狀況

useState 咱們不可能只使用一個,當咱們使用多個 useState 的時候,那 react 是如何識別那個是哪一個呢,其實很簡單,它是靠第一次執行的順序來記錄的,就至關於每一個組件存放useState 的地方是一個數組,每使用一個新的 useState,就向數組中 push 一個 useState,那麼固然,當咱們在運行時改變、添加、減小 useState 時,react 還能正常執行嗎

function App (props) {
  let count, setCount
  let sum, setSum
  if (count > 2) {
    [ count, setCount ] = useState(0)
    [ sum, setSum ] = useState(10)
  } else {
    [ sum, setSum ] = useState(10)
    [ count, setCount ] = useState(0)
  }
  return (
    <div> 點擊次數: { count } 總計:{ sum } <button onClick={() => { setCount(count + 1); setSum(sum - 1)}}>點我</button> </div>
    )
}
複製代碼

當咱們在運行時改變 useState 的順序,數據會混亂,增長 useState, 程序會報錯

不要在循環,條件或嵌套函數中調用 Hook, 確保老是在你的 React 函數的最頂層調用他們。遵照這條規則,你就能確保 Hook 在每一次渲染中都按照一樣的順序被調用。這讓 React 可以在屢次的 useStateuseEffect 調用之間保持 hook 狀態的正確

同時推薦使用 eslint-plugin-react-hooks 插件來規範代碼編寫,針對這種狀況進行校驗

useState 的使用就是這麼簡單,我已經學會了, 接下來,咱們看一下 useEffect 的使用

useEffect 的使用

Effect Hook 可讓你在函數組件中執行反作用操做,這裏提到反作用,什麼是反作用呢,就是除了狀態相關的邏輯,好比網絡請求,監聽事件,查找 dom

簡單例子

有這樣一個需求,須要咱們在組件在狀態更新的時候改變 document.title,在之前咱們會這樣寫代碼

class App extends PureComponent {
    state = {
      count: 0
    }

    componentDidMount() {
      document.title = count
    }

    componentDidUpdate() {
      document.title = count
    }
    render () {
      const { count } = this.state
      return (
        <div> 頁面名稱: { count } <button onClick={() => { this.setState({ count: count++ })}}>點我</button> </div>
      )
    }
  }
複製代碼

使用 hooks 怎麼寫呢

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

  useEffect(() => {
    document.title = count
  })

  return (
    <div> 頁面名稱: { count } <button onClick={() => { setCount(count + 1 )}}>點我</button> </div>
    )
}
複製代碼

useEffect 是什麼呢,咱們先忽略,回到咱們總結的 class 組件存在的問題,useState 只是讓咱們的函數組件具備使用 state 的能力,那咱們要解決 class 組件存在的問題,先來解決第一個,生命週期臃腫的問題

useEffect 生命週期

若是你熟悉 React class 的生命週期函數,你能夠把 useEffect Hook 看作 componentDidMountcomponentDidUpdatecomponentWillUnmount 這三個函數的組合。

以往咱們在綁定事件、解綁事件、設定定時器、查找 dom 的時候,都是經過 componentDidMountcomponentDidUpdatecomponentWillUnmount 生命週期來實現的,而 useEffect 會在組件每次 render 以後調用,就至關於這三個生命週期函數,只不過能夠經過傳參來決定是否調用

其中注意的是,useEffect 會返回一個回調函數,做用於清除上一次反作用遺留下來的狀態,若是該 useEffect 只調用一次,該回調函數至關於 componentWillUnmount 生命週期

具體看下面例子

function App () {
    const [ count, setCount ] = useState(0)
    const [ width, setWidth ] = useState(document.body.clientWidth)

    const onChange = () => {
      
      setWidth(document.body.clientWidth)
    }

    useEffect(() => {
      window.addEventListener('resize', onChange, false)

      return () => {
        window.removeEventListener('resize', onChange, false)
      }
    })

    useEffect(() => {
      document.title = count
    })

    return (
      <div> 頁面名稱: { count } 頁面寬度: { width } <button onClick={() => { setCount(count + 1)}}>點我</button> </div>
      )
  }
複製代碼

接着咱們前面的簡單例子,咱們上面例子要處理兩種反作用邏輯,這裏咱們既要處理 title,還要監聽屏幕寬度改變,按照 class 的寫法,咱們要在生命週期中處理這兩種邏輯,但在 hooks 中,咱們只須要兩個 useEffect 就能解決這些問題,咱們以前提到,useEffect 可以返回一個函數,用來清除上一次反作用留下的狀態,這個地方咱們能夠用來解綁事件監聽,這個地方存在一個問題,就是 useEffect 是每次 render 以後就會調用,好比 title 的改變,至關於 componentDidUpdate,但咱們的事件監聽不該該每次 render 以後,進行一次綁定和解綁,就是咱們須要 useEffect 變成 componentDidMount, 它的返回函數變成 componentWillUnmount,這裏就須要用到 useEffect 函數的第二個參數了

useEffect 的第二個參數

useEffect 的第二個參數,有三種狀況

  1. 什麼都不傳,組件每次 render 以後 useEffect 都會調用,至關於 componentDidMountcomponentDidUpdate
  2. 傳入一個空數組 [], 只會調用一次,至關於 componentDidMountcomponentWillUnmount
  3. 傳入一個數組,其中包括變量,只有這些變量變更時,useEffect 纔會執行

具體看下面例子

function App () {
    const [ count, setCount ] = useState(0)
    const [ width, setWidth ] = useState(document.body.clientWidth)

    const onChange = () => {
      setWidth(document.body.clientWidth)
    }

    useEffect(() => {
      // 至關於 componentDidMount
      console.log('add resize event')
      window.addEventListener('resize', onChange, false)

      return () => {
        // 至關於 componentWillUnmount
        window.removeEventListener('resize', onChange, false)
      }
    }, [])

    useEffect(() => {
      // 至關於 componentDidUpdate
      document.title = count
    })

    useEffect(() => {
      console.log(`count change: count is ${count}`)
    }, [ count ])

    return (
      <div> 頁面名稱: { count } 頁面寬度: { width } <button onClick={() => { setCount(count + 1)}}>點我</button> </div>
      )
  }
複製代碼

根據上面的例子的運行結果,第一個 useEffect 中的 'add resize event' 只會在第一次運行時輸出一次,不管組件怎麼 render,都不會在輸出,第二個 useEffect 會在每次組件 render 以後都執行,title 每次點擊都會改變, 第三個 useEffect, 只要有在第一次運行和 count 改變時,纔會執行,屏幕發生改變引發的 render 並不會影響第三個 useEffect

useContext

關於 react 中如何使用 context,這裏就不細說,能夠看我以前寫的 React 中 Context 的使用

context 中的 ProviderConsumer,在類組件和函數組件中都能使用,contextType 只能在類組件中使用,由於它是類的靜態屬性,具體如何使用 useContext 呢,看下面的例子

// 建立一個 context
const Context = createContext(0)

// 組件一, Consumer 寫法
class Item1 extends PureComponent {
  render () {
    return (
      <Context.Consumer>
        {
          (count) => (<div>{count}</div>)
        }
      </Context.Consumer>
    )
  }
}
// 組件二, contextType 寫法
class Item2 extends PureComponent {
  static contextType = Context
  render () {
    const count = this.context
    return (
      <div>{count}</div>
    )
  }
}
// 組件一, useContext 寫法
function Item3 () {
  const count = useContext(Context);
  return (
    <div>{ count }</div>
  )
}

function App () {
  const [ count, setCount ] = useState(0)
  return (
    <div>
      點擊次數: { count } 
      <button onClick={() => { setCount(count + 1)}}>點我</button>
      <Context.Provider value={count}>
        <Item1></Item1>
        <Item2></Item2>
        <Item3></Item3>
      </Context.Provider>
    </div>
    )
}
複製代碼

經過運行上面的例子,咱們獲得的結果是,三種寫法都可以實現咱們的需求,可是,三種寫有各自的優缺點,下面爲對比出的結果

寫法 優缺點
consumer 嵌套複雜,Consumer 第一個子節點必須爲一個函數,無形增長了工做量
contextType 只支持 類組件,沒法在多 context 的狀況下使用
useContext 不須要嵌套,多 context 寫法簡單

經過上面的比較,沒理由繼續使用 consumercontextType

useMemo

useMemo 是什麼呢,它跟 memo 有關係嗎, memo 的具體內容能夠查看 React 中性能優化、 memo、PureComponent、shouldComponentUpdate 的使用,說白了 memo 就是函數組件的 PureComponent,用來作性能優化的手段,useMemo 也是,useMemo 在個人印象中和 Vuecomputed 計算屬性相似,都是根據依賴的值計算出結果,當依賴的值未發生改變的時候,不觸發狀態改變,useMemo 具體如何使用呢,看下面例子

function App () {
  const [ count, setCount ] = useState(0)
  const add = useMemo(() => {
    return count + 1
  }, [count])
  return (
    <div> 點擊次數: { count } <br/> 次數加一: { add } <button onClick={() => { setCount(count + 1)}}>點我</button> </div>
    )
}
複製代碼

上面的例子中,useMemo 也支持傳入第二個參數,用法和 useEffect 相似

  1. 不傳數組,每次更新都會從新計算
  2. 空數組,只會計算一次
  3. 依賴對應的值,當對應的值發生變化時,纔會從新計算(能夠依賴另一個 useMemo 返回的值)

須要注意的是,useMemo 會在渲染的時候執行,而不是渲染以後執行,這一點和 useEffect 有區別,因此 useMemo 不建議有 反作用相關的邏輯

同時,useMemo 能夠做爲性能優化的手段,但不要把它當成語義上的保證,未來,React 可能會選擇「遺忘」之前的一些 memoized 值,並在下次渲染時從新計算它們

useCallback

useCallback 是什麼呢,能夠說是 useMemo 的語法糖,能用 useCallback 實現的,均可以使用 useMemo, 在 react 中咱們常常面臨一個子組件渲染優化的問題,細節能夠查看React 中性能優化、 memo、PureComponent、shouldComponentUpdate 的使用,尤爲是在向子組件傳遞函數props時,每次 render 都會建立新函數,致使子組件沒必要要的渲染,浪費性能,這個時候,就是 useCallback 的用武之地了,useCallback 能夠保證,不管 render 多少次,咱們的函數都是同一個函數,減少不斷建立的開銷,具體如何使用看下面例子

const onClick = `useMemo`(() => {
  return () => {
    console.log('button click')
  }
}, [])

const onClick = useCallback(() => {
 console.log('button click')
}, [])
複製代碼

一樣,useCallback 的第二個參數和useMemo同樣,沒有區別

useRef

useRef 有什麼做用呢,其實很簡單,總共有兩種用法

  1. 獲取子組件的實例(只有類組件可用)
  2. 在函數組件中的一個全局變量,不會由於重複 render 重複申明, 相似於類組件的 this.xxx

獲取子組件實例

上面提到了一點,useRef 只能獲取子組件的實例,這在類組件中也是一樣的道理,具體看下面的例子

// 使用 ref 子組件必須是類組件
class Children extends PureComponent {
  render () {
    const { count } = this.props
    return (
      <div>{ count }</div>
    )
  }
}

function App () {
  const [ count, setCount ] = useState(0)
  const childrenRef = useRef(null)
  // const 
  const onClick = useMemo(() => {
    return () => {
      console.log('button click')
      console.log(childrenRef.current)
      setCount((count) => count + 1)
    }
  }, [])
  return (
    <div> 點擊次數: { count } <Children ref={childrenRef} count={count}></Children> <button onClick={onClick}>點我</button> </div>
    )
}
複製代碼

useRef 在使用的時候,能夠傳入默認值來指定默認值,須要使用的時候,訪問 ref.current 便可訪問到組件實例

類組件屬性

有些狀況下,咱們須要保證函數組件每次 render 以後,某些變量不會被重複申明,好比說 Dom 節點,定時器的 id 等等,在類組件中,咱們徹底能夠經過給類添加一個自定義屬性來保留,好比說 this.xxx, 可是函數組件沒有 this,天然沒法經過這種方法使用,有的朋友說,我可使用 useState 來保留變量的值,可是 useState 會觸發組件 render,在這裏徹底是不須要的,咱們就須要使用 useRef 來實現了,具體看下面例子

function App () {
  const [ count, setCount ] = useState(0)
  const timer = useRef(null)
  let timer2 
  
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count => count + 1)
    }, 500)

    timer.current = id
    timer2 = id
    return () => {
      clearInterval(timer.current)
    }
  }, [])

  const onClickRef = useCallback(() => {
    clearInterval(timer.current)
  }, [])

  const onClick = useCallback(() => {
    clearInterval(timer2)
  }, [])

  return (
    <div> 點擊次數: { count } <button onClick={onClick}>普通</button> <button onClick={onClickRef}>useRef</button> </div>
    )
}
複製代碼

當咱們們使用普通的按鈕去暫停定時器時發現定時器沒法清除,由於 App 組件每次 render,都會從新申明一次 timer2, 定時器的 id 在第二次 render 時,就丟失了,因此沒法清除定時器,針對這種狀況,就須要使用到 useRef,來爲咱們保留定時器 id,相似於 this.xxx,這就是 useRef 的另一種用法

useReducer

useReducer 是什麼呢,它其實就是相似 redux 中的功能,相較於 useState,它更適合一些邏輯較複雜且包含多個子值,或者下一個 state 依賴於以前的 state 等等的特定場景, useReducer 總共有三個參數

  1. 第一個參數是 一個 reducer,就是一個函數相似 (state, action) => newState 的函數,傳入 上一個 state 和本次的 action
  2. 第二個參數是初始 state,也就是默認值,是比較簡單的方法
  3. 第三個參數是惰性初始化,這麼作能夠將用於計算 state 的邏輯提取到 reducer 外部,這也爲未來對重置 stateaction 作處理提供了便利

具體使用方法看下面的例子

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 App() {
  const [state, dispatch] = useReducer(reducer, {
    count: 0
  });
  return (
    <> 點擊次數: {state.count} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } 複製代碼

useImperativeHandle

useImperativeHandle 可讓你在使用 ref 時自定義暴露給父組件的實例值,說簡單點就是,子組件能夠選擇性的暴露給副組件一些方法,這樣能夠隱藏一些私有方法和屬性,官方建議,useImperativeHandle應當與 forwardRef 一塊兒使用,具體如何使用看下面例子

function Kun (props, ref) {
  const kun = useRef()

  const introduce = useCallback (() => {
    console.log('i can sing, jump, rap, play basketball')
  }, [])
  useImperativeHandle(ref, () => ({
    introduce: () => {
      introduce()
    }
  }));

  return (
    <div ref={kun}> { props.count }</div>
  )
}

const KunKun = forwardRef(Kun)

function App () {
  const [ count, setCount ] = useState(0)
  const kunRef = useRef(null)

  const onClick = useCallback (() => {
    setCount(count => count + 1)
    kunRef.current.introduce()
  }, [])
  return (
    <div> 點擊次數: { count } <KunKun ref={kunRef} count={count}></KunKun> <button onClick={onClick}>點我</button> </div>
    )
}
複製代碼

其它hook

還有兩個 hook,沒什麼好講的,用的也很少,能夠看看官方文檔

  1. useLayoutEffect
  2. useDebugValue

自定義hook

咱們以前總結出三個問題,class this 指向問題,生命週期邏輯冗餘問題,都已獲得解決,而邏輯難以複用,在前面的例子中並無解決,要解決這個問題,就要經過 自定義 hook 來解決

自定義 Hook,能夠將組件邏輯提取到可重用的函數中,來解決邏輯難以複用問題

前面有個例子是獲取屏幕寬度變化的例子,假設咱們有諸多組件都須要這個邏輯,那麼咱們只須要將其抽取成一個自定義 hook 便可,具體實現看下面例子

function useWidth (defaultWidth) {
  const [width, setWidth] = useState(document.body.clientWidth)

  const onChange = useCallback (() => {
    setWidth(document.body.clientWidth)
  }, [])

  useEffect(() => {
    window.addEventListener('resize', onChange, false)

    return () => {
      window.removeEventListener('resize', onChange, false)
    }
  }, [onChange])

  return width
}

function App () {

  const width = useWidth(document.body.clientWidth)

  return (
    <div> 頁面寬度: { width } </div>
    )
}
複製代碼

經過上面的例子,咱們能夠看出

自定義 hook 是一個函數,其名稱以 use 開頭,函數內部能夠調用其餘的 hook,至於爲何要以 use 開頭,是由於若是不以 use 開頭,React 就沒法判斷某個函數是否包含對其內部 hook 的調用,React 也將沒法自動檢查你的 hook 是否違反了 hook 的規則,因此要以 use 開頭

自定義 hook,真的很簡單,不過具體什麼樣的邏輯,須要抽離成自定義 hook,這就須要工做中不段積累的經驗去判斷,避免爲了 hookhook

總結

在我學習和使用自定義 hook 時,我發現其實它的道理很簡單,不少前端框架、庫裏面都有相似的概念,框架和庫的設計最後都疏通同歸,因此咱們在學習一個新的框架、庫或者理念時,不該該將其是爲一個全新的東西,而更多的應該從自身掌握的內容去推導,去舉一反三,這樣咱們在學習的時候會事半功倍,在日益更新的前端領域,可以抽出更多的時間去理解更爲核心的內容

最後,若是本文對你有任何幫助的話,感謝點個贊 💗

參考

  1. react-1251415695.cos-website.ap-chengdu.myqcloud.com/docs/hooks-…
  2. reactjs.org/docs/hooks-…

react 其餘文章

  1. React 中 lazy, Suspense 以及錯誤邊界(Error Boundaries)的使用
  2. React 中 Context 的使用
  3. React 中性能優化、 memo、 PureComponent、shouldComponentUpdate 的使用
相關文章
相關標籤/搜索