React Hooks 加持下的函數組件設計

有了 React Hooks 的加持,媽媽不再用擔憂函數組件記不住狀態

過去,React 中的函數組件都被稱爲無狀態函數式組件(stateless functional component),這是由於函數組件沒有辦法擁有本身的狀態,只能根據 Props 來渲染 UI ,其性質就至關因而類組件中的 render 函數,雖然結構簡單明瞭,可是做用有限。javascript

但自從 React Hooks 橫空出世,函數組件也擁有了保存狀態的能力,並且也逐漸可以覆蓋到類組件的應用場景,所以能夠說 React Hooks 就是將來 React 發展的方向。java

React Hooks 解決了什麼問題

複雜的組件難以分拆

咱們知道組件化的思想就是將一個複雜的頁面/大組件,按照不一樣層次,逐漸抽象並拆分紅功能更純粹的小組件,這樣一方面能夠減小代碼耦合,另一方面也能夠更好地複用代碼;但實際上,在使用 React 的類組件時,每每難以進一步分拆複雜的組件,這是由於邏輯是有狀態的,若是強行分拆,會令代碼複雜性急劇上升;如使用 HOC 和 Render Props 等設計模式,這會造成「嵌套地獄」,使咱們的代碼變得晦澀難懂。react

狀態邏輯複雜,給單元測試形成障礙

這其實也是上一點的延續:要給一個擁有衆多狀態邏輯的組件寫單元測試,無疑是一件使人崩潰的事情,由於須要編寫大量的測試用例來覆蓋代碼執行路徑。git

組件生命週期繁複

對於類組件,咱們須要在組件提供的生命週期鉤子中處理狀態的初始化、數據獲取、數據更新等操做,處理起來自己邏輯就比較複雜,並且各類「反作用」混在一塊兒也令人頭暈目眩,另外還極可能忘記在組件狀態變動/組件銷燬時消除反作用。github

React Hooks 就是來解決以上這些問題的

  • 針對狀態邏輯分拆複用難的問題:其實並非 React Hooks 解決的,函數這一形式自己就具備邏輯簡單、易複用等特性。
  • 針對組件生命週期繁複的問題:React Hooks 屏蔽了生命週期這一律念,一切的邏輯都是由狀態驅動,或者說由數據驅動的,那麼理解、處理起來就簡單多了。

利用自定義 Hooks 捆綁封裝邏輯與相關 state

我認爲 React Hooks 的亮點不在於 React 官方提供的那些 API ,那些 API 只是一些基礎的能力;其亮點仍是在於自定義 Hooks —— 一種封裝複用的設計模式。web

例如,一個頁面上每每有不少狀態,這些狀態分別有各自的處理邏輯,若是用類組件的話,這些狀態和邏輯都會混在一塊兒,不夠直觀:設計模式

class Com extends React.Component {
    state = {
        a: 1,
        b: 2,
        c: 3,
    }
    
    componentDidMount() {
        handleA()
        handleB()
        handleC()
    }
}
複製代碼

而使用 React Hooks 後,咱們能夠把狀態和邏輯關聯起來,分拆成多個自定義 Hooks ,代碼結構就會更清晰:api

function useA() {
    const [a, setA] = useState(1)
    useEffect(() => {
        handleA()
    }, [])
    
    return a
}

function useB() {
    const [b, setB] = useState(2)
    useEffect(() => {
        handleB()
    }, [])
    
    return b
}

function useC() {
    const [c, setC] = useState(3)
    useEffect(() => {
        handleC()
    }, [])
    
    return c
}

function Com() {
    const a = useA()
    const b = useB()
    const c = useC()
}
複製代碼

咱們除了能夠利用自定義 Hooks 來拆分業務邏輯外,還能夠拆分紅複用價值更高的通用邏輯,好比說目前比較流行的 Hooks 庫:react-use;另外,React 生態中原來的不少庫,也開始提供 Hooks API ,如 react-router數組

忘記組件生命週期吧

React 提供了大量的組件生命週期鉤子,雖然在平常業務開發中,用到的很少,但光是 componentDidUpdate 和 componentWillUnmount 就讓人很頭痛了,一不留神就忘記處理 props 更新組件銷燬須要處理反作用的場景,這不只會留下肉眼可見的 bug ,還會留下一些內存泄露的隱患。性能優化

類 MVVM 框架講究的是數據驅動,而生命週期這種設計模式,就明顯更偏向於傳統的事件驅動模型;當咱們引入 React Hooks 後,數據驅動的特性可以變得更純粹。

處理 props 更新

下面咱們以一個很是典型的列表頁面來舉個例子:

class List extends Component {
  state = {
    data: []
  }
  fetchData = (id, authorId) => {
    // 請求接口
  }
  componentDidMount() {
    this.fetchData(this.props.id, this.props.authorId)
    // ...其它不相關的初始化邏輯
  }
  componentDidUpdate(prevProps) {
    if (
      this.props.id !== prevProps.id ||
      this.props.authorId !== prevProps.authorId // 別漏了!
    ) {
      this.fetchData(this.props.id, this.props.authorId)
    }
    
    // ...其它不相關的更新邏輯
  }
  render() {
    // ...
  }
}
複製代碼

上面這段代碼有3個問題:

  • 須要同時在兩個生命週期裏執行幾乎相同的邏輯。
  • 在判斷是否須要更新數據的時候,容易漏掉依賴的條件。
  • 每一個生命週期鉤子裏,會散落大量不相關的邏輯代碼,違反了高內聚的原則,影響閱讀代碼的連貫性。

若是改爲用 React Hooks 來實現,問題就能獲得很大程度上的解決了:

function List({ id, authorId }) {
    const [data, SetData] = useState([])
    const fetchData = (id, authorId) => {}
    useEffect(() => {
        fetchData(id, authorId)
    }, [id, authorId])
}
複製代碼

改用 React Hooks 後:

  • 咱們不須要考慮生命週期,咱們只須要把邏輯依賴的狀態都丟進依賴列表裏, React 會幫咱們判斷何時該執行的。
  • React 官方提供了 eslint 的插件來檢查依賴項列表是否完整。
  • 咱們可使用多個 useEffect ,或者多個自定義 Hooks 來區分開多個無關聯的邏輯代碼段,保障高內聚特性。

處理反作用

最多見的反作用莫過於綁定 DOM 事件:

class List extends React.Component {
    handleFunc = () => {}
    componentDidMount() {
        window.addEventListener('scroll', this.handleFunc)
    }
    componentWillUnmount() {
        window.removeEventListener('scroll', this.handleFunc)
    }
}
複製代碼

這塊也仍是會有上述說的,影響高內聚的問題,改爲 React Hooks :

function List() {
    useEffect(() => {
        window.addEventListener('scroll', this.handleFunc)
    }, () => {
        window.removeEventListener('scroll', this.handleFunc)
    })
}
複製代碼

並且比較絕的是,除了在組件銷燬的時候會觸發外,在依賴項變化的時候,也會執行清除上一輪的反作用。

利用 useMemo 作局部性能優化

在使用類組件的時候,咱們須要利用 componentShouldUpdate 這個生命週期鉤子來判斷當前是否須要從新渲染,而改用 React Hooks 後,咱們能夠利用 useMemo 來判斷是否須要從新渲染,達到局部性能優化的效果:

function List(props) => {
  useEffect(() => {
    fetchData(props.id)
  }, [props.id])

  return useMemo(() => (
    // ...
  ), [props.id])
}
複製代碼

在上面這段代碼中,咱們看到最終渲染的內容是依賴於props.id,那麼只要props.id不變,即使其它 props 再怎麼辦,該組件也不會從新渲染。

依靠 useRef 擺脫閉包

在咱們剛開始使用 React Hooks 的時候,常常會遇到這樣的場景:在某個事件回調中,須要根據當前狀態值來決定下一步執行什麼操做;但咱們發現事件回調中拿到的老是舊的狀態值,而不是最新狀態值,這是怎麼回事呢?

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

  const log = () => {
    setCount(count + 1);
    setTimeout(() => {
      console.log(count);
    }, 3000);
  };

  return (
    <button onClick={log}>報數</button>
  );
}

/* 若是咱們在三秒內連續點擊三次,那麼count的值最終會變成 3,而隨之而來的輸出結果是? 0 1 2 */

複製代碼

「這是 feature 不是 bug 」,哈哈哈,說是 feature 可能也不太準確,由於這不正是 javascript 閉包的特性嗎?當咱們每次往setTimeout裏傳入回調函數時,這個回調函數都會引用下當前函數做用域(此時 count 的值還未被更新),因此在執行的時候打印出來的就會是舊的狀態值。

類組件是怎麼實現的?

那爲啥類組件中,每次都能取到最新的狀態值呢?這是由於咱們在類組件中取狀態值都是從this.state裏取的,這至關因而類組件的一個執行上下文,永遠都是保持最新的。

藉助 useRef 共享修改

經過useRef建立的對象,其值只有一份,並且在全部 Rerender 之間共享

聽上去,這 useRef 其實跟 this.state 很類似嘛,都是一個能夠一直維持的值,那咱們就能夠用它來維護咱們的狀態了:

function Counter() {
  const count = useRef(0);

  const log = () => {
    count.current++;
    setTimeout(() => {
      console.log(count.current);
    }, 3000);
  };

  return (
    <button onClick={log}>報數</button>
  );
}

/* 3 3 3 */
複製代碼
相關文章
相關標籤/搜索