淺談React Hooks

因爲工做的緣由我已經很長時間沒接觸過React了。前段時間圈子裏都在討論React Hooks,出於好奇也學習了一番,特此整理以加深理解。javascript

原因

在web應用無所不能的9012年,組成應用的Components也愈來愈複雜,冗長而難以複用的代碼給開發者們形成了不少麻煩。好比:html

  1. 難以複用stateful的代碼,render props及HOC雖然解決了問題,但對組件的包裹改變了組件樹的層級,存在冗餘;
  2. 在ComponentDidMount、ComponentDidUpdate、ComponentWillUnmount等生命週期中作獲取數據,訂閱/取消事件,操做ref等相互之間無關聯的操做,而把訂閱/取消這種相關聯的操做分開,下降了代碼的可讀性;
  3. 與其餘語言中的class概念差別較大,須要對事件處理函數作bind操做,使人困擾。另外class也不利於組件的AOT compile,minify及hot loading。 在這種背景下,React在16.8.0引入了React Hooks。

特性

主要介紹state hook,effect hook及custom hookjava

State Hook

最基本的應用以下:react

import React, { useState } from 'react'

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

  return (
    <div> <p>You have clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click </button> </div>
  )
}
複製代碼

調用useState,傳入初始值,經過數組的結構賦值獲得獨立的local state count,及setCount。count能夠理解爲class component中的state,可見這裏的state不侷限於對象,能夠爲number,string,固然也能夠是一個對象。而setCount能夠理解爲class component中的setState,不一樣的是setState會merge新老state,而hook中的set函數會直接替換,這就意味着若是state是對象時,每次set應該傳入全部屬性,而不能像class component那樣僅傳入變化的值。因此在使用useState時,儘可能將相關聯的,會共同變化的值放入一個object。web

再看看有多個「local state」的狀況:數組

import React, { useState } from 'react'

function person() {
  const [name, setName] = useState('simon')
  const [age, setAge] = useState(24)

  return (
    <div> <p>name: {name}</p> <p>age: {age}</p> </div>
  )
}
複製代碼

咱們知道當函數執行完畢,函數做用域內的變量都會銷燬,hooks中的state在component首次render後被React保留下來了。那麼在下一次render時,React如何將這些保留的state與component中的local state對應起來呢。這裏給出一個簡單版本的實現:異步

const stateArr = []
const setterArr = []
let cursor = 0
let isFirstRender = true

function createStateSetter(cursor) {
  return state => {
    stateArr[cursor] = state
  }
}

function useState(initState) {
  if (isFirstRender) {
    stateArr.push(initState)
    setterArr.push(createStateSetter(cursor))

    isFirstRender = false
  }

  const state = stateArr[cursor]
  const setter = setterArr[cursor]

  cursor++

  return [state, setter]
}
複製代碼

能夠看出React須要保證多個hooks在component每次render的時候的執行順序都保持一致,不然就會出現錯誤。這也是React hooks rule中必須在top level使用hooks的由來————條件,遍歷等語句都有可能會改變hooks執行的順序。ide

Effect Hook

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

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null)

  // 基本寫法
  useEffect(() => {
    document.title = 'Dom is ready'
  })

  // 須要取消操做的寫法
  useEffect(() => {
    function handleStatusChange(status) {
        setIsOnline(status.isOnline)
    }
    
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)

    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange)
    }
  })

  if (isOnline === null) {
    return 'Loading...'
  }
  return isOnline ? 'Online' : 'Offline'
}
複製代碼

能夠看到上面的代碼在傳入useEffect的函數(effect)中作了一些"side effect",在class component中咱們一般會在componentDidMount,componentDidUpdate中去作這些事情。另外在class component中,須要在componentDidMount中訂閱,在componentWillUnmount中取消訂閱,這樣將一件事拆成兩件事作,不只可讀性低,還容易產生bug:函數

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}
複製代碼

如上代碼,若是props中的friend.id發生變化,則會致使訂閱和取消的id不一致,如需解決須要在componentDidUpdate中先取消訂閱舊的再訂閱新的,代碼很是冗餘。而useEffect hook在這一點上是渾然天成的。另外effect函數在每次render時都是新建立的,這實際上是有意而爲之,由於這樣才能取得最新的state值。工具

有同窗可能會想,每次render後都會執行effect,這樣會不會對性能形成影響。其實effect是在頁面渲染完成以後執行的,不會阻塞,而在effect中執行的操做每每不要求同步完成,除了少數如要獲取寬度或高度,這種狀況須要使用其餘的hook(useLayoutEffect),此處不作詳解。即便這樣,React也提供了控制的方法,及useEffect的第二個參數————一個數組,若是數組中的值不發生變化的話就跳過effect的執行:

useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  }
}, [props.friend.id])
複製代碼

Custom Hook

A custom Hook is a JavaScript function whose name starts with 」use」 and that may call other Hooks. Custom Hook的使命是解決stateful logic複用的問題,如上面例子中的FriendStatus,在一個聊天應用中可能多個組件都須要知道好友的在線狀態,將FriendStatus抽象成這樣的hook:

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
        setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id)

  if (isOnline === null) {
    return 'Loading...'
  }

  return isOnline ? 'Online' : 'Offline'
}

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id)

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li>
  )
}
複製代碼

FriendStatus和FriendListItem中的isOnline是獨立的,因custom hook複用的是stateful logic,而不是state自己。另外custom hook必須以use開頭來命名,這樣linter工具才能正確檢測其是否符合規範。

除了以上三種hook,React還提供了useContext, useReducer, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect, useDebugValue內置hook,它們的用途能夠參考官方文檔,這裏我想單獨講講useRef。 顧名思義,這個hook應該跟ref相關的:

function TextInputWithFocusButton() {
  const inputEl = useRef(null)
  const onButtonClick = () => {
    inputEl.current.focus()
  }
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  )
}
複製代碼

來看看官方文檔上的說明:

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component. 這句話告訴咱們在組件的整個生命週期裏,inputEl.current都是存在的,這擴展了useRef自己的用途,可使用useRef維護相似於class component中實例屬性的變量:

function Timer() {
  const intervalRef = useRef()

  useEffect(() => {
    const id = setInterval(() => {
      // ...
    })
    intervalRef.current = id
    return () => {
      clearInterval(intervalRef.current)
    }
  })

  // ...
}
複製代碼

這在class component中是理所固然的,但不要忘記Timer僅僅是一個函數,函數執行完畢後函數做用域內的變量將會銷燬,因此這裏須要使用useRef來保持這個timerId。相似的useRef還能夠用來獲取preState:

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

  const prevCountRef = useRef()
  useEffect(() => {
    prevCountRef.current = count    // 因爲useEffect中的函數是在render完成以後異步執行的,因此在每次render時prevCountRef.current的值爲上一次的count值
  })
  const prevCount = prevCountRef.current

  return <h1>Now: {count}, before: {prevCount}</h1>
}
複製代碼

參考文章&拓展閱讀

相關文章
相關標籤/搜索