天天看點

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

有了 react Hooks 的加持,媽媽再也不用擔心函數元件記不住狀态

過去,react 中的函數元件都被稱為無狀态函數式元件(stateless functional component),這是因為函數元件沒有辦法擁有自己的狀态,隻能根據 Props 來渲染 UI ,其性質就相當于是類元件中的 render 函數,雖然結構簡單明了,但是作用有限。

但自從 React Hooks 橫空出世,函數元件也擁有了儲存狀态的能力,而且也逐漸能夠覆寫到類元件的應用場景,是以可以說 React Hooks 就是未來 React 發展的方向。

React Hooks 解決了什麼問題

複雜的元件難以分拆

我們知道元件化的思想就是将一個複雜的頁面/大元件,按照不同層次,逐漸抽象并拆分成功能更純粹的小元件,這樣一方面可以減少代碼耦合,另外一方面也可以更好地複用代碼;但實際上,在使用 React 的類元件時,往往難以進一步分拆複雜的元件,這是因為邏輯是有狀态的,如果強行分拆,會令代碼複雜性急劇上升;如使用 HOC 和 Render Props 等設計模式,這會形成“嵌套地獄”,使我們的代碼變得晦澀難懂。

狀态邏輯複雜,給單元測試造成障礙

這其實也是上一點的延續:要給一個擁有衆多狀态邏輯的元件寫單元測試,無疑是一件令人崩潰的事情,因為需要編寫大量的測試用例來覆寫代碼執行路徑。

元件生命周期繁複

對于類元件,我們需要在元件提供的生命周期鈎子中處理狀态的初始化、資料擷取、資料更新等操作,處理起來本身邏輯就比較複雜,而且各種“副作用”混在一起也使人頭暈目眩,另外還很可能忘記在元件狀态變更/元件銷毀時消除副作用。

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

  • 針對狀态邏輯分拆複用難的問題:其實并不是 React Hooks 解決的,函數這一形式本身就具有邏輯簡單、易複用等特性。
  • 針對元件生命周期繁複的問題:React Hooks 屏蔽了生命周期這一概念,一切的邏輯都是由狀态驅動,或者說由資料驅動的,那麼了解、處理起來就簡單多了。

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

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

例如,一個頁面上往往有很多狀态,這些狀态分别有各自的處理邏輯,如果用類元件的話,這些狀态和邏輯都會混在一起,不夠直覺:

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

而使用 React Hooks 後,我們可以把狀态和邏輯關聯起來,分拆成多個自定義 Hooks ,代碼結構就會更清晰:

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 再怎麼辦,該元件也不會重新渲染。

vi設計http://www.maiqicn.com 辦公資源網站大全https://www.wode007.com

依靠 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
 */
           

類元件是怎麼實作的?

借助 useRef 共享修改

通過useRef建立的對象,其值隻有一份,而且在所有 Rerender 之間共享。
function Counter() {
  const count = useRef(0);

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

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

/*
    3
    3
    3
 */           

繼續閱讀