React hooks詳解

  • 2020 年 3 月 18 日
  • 筆記

此篇文章僅是對hooks入門的總結,老鳥略過吧~

React從16.8.X以後增加了一個新特性,react hooks 讓我們看看這個新特性又帶來了哪些驚喜呢~以下內容我們採取不同方式創建組件來進行對比總結

組件的創建方式:

用過react的都了解,傳統react創建組件提供了兩種方式,函數式與類(class)

class創建無狀態組件

class App extends React.Component {    constructor(props) {      super(props);    }      render() {      return <div>        <p>{this.props.name}</p>      </div>    }  }    function renderApp() {    let appProps = {      name: 'dqhan'    }    ReactDOM.render(      <App  {...appProps} />,      document.getElementById('app')    )  }    renderApp();

添加狀態管理

class App extends React.Component {    constructor(props) {      super(props);      this.state = {        name: props.name      };      this.handleChangeName = this.handleChangeName.bind(this);    }      handleChangeName() {      this.setState({        name: '我變了'      })    }      render() {      return (        <React.Fragment>          <p> {`hello~${this.state.name}`} </p>          <button onClick={this.handleChangeName}></button>        </React.Fragment>      );    }  }

我們通過class可以實現無狀態組件以及常規組件通過setState的狀態管理。

函數式創建組件

function App(props) {    return <p>{`hello! ${props.name}`}</p>  }    function renderApp() {    let appProps = { name: "dqhan" };    ReactDOM.render(<App {...appProps} />, document.getElementById("app"));  }

函數式創建組件通常是無狀態組件,這種方式沒有辦法在內部對狀態統一管理,如果我們非要添加狀態管理呢,那就只能藉助redux啦~或者我們自己利用觀察者模式實現一個發布訂閱(emmmm比較勉強吧,畢竟實際開發中我們不可能這麼做)

那麼如果我們非要這麼做呢,正題來了,React版本在16.8.X以後增添了一個新特性就是hooks。

hooks涉及API有useState、 useEffect、 useCallback、 useRef、 useMemo、 React.memo、 useReducer等,具體可以參考官方文檔,我們來看一下hooks怎麼用

React Hooks創建組件

無狀態組件

function App(props) {    return <div>      <p>{`hello~${props.name}`}</p>    </div>  }

哈哈,跟函數式一樣,畢竟我們要在函數式組件里添加狀態管理嘛

1.useState

作用:添加狀態管理

function App() {    let [name, setName] = useState('dqhan');    return <div>      <p>{`hello~${name}`}</p>      <button onClick={() => setName('我變了')}>Click</button>    </div>;  }

react hook可以管理自己的狀態,有自己的函數鉤子,這點相比要函數式顯然效果更好,不需要藉助redux,這就是我們為啥要在前面提到函數式編程涉及狀態管理問題,就是要在這裡跟react hook做個比較。

到這裡我們知道了這個useState,多了一個useState讓函數式創建類有了自己的持久狀態。那麼在函數式裡面我們如何做到class組件中的setState呢?

function App(props) {    let [name, setName] = useState('dqhan');    let handleChangeName = useCallback(() => {      setName(preState => {        let updatedValues = {          newValue: '我變了'        }        return { ...{ preState }, ...updatedValues }      })    })    function click1(params) {      setName('我變了1')    }    return <div>      <p>{`hello~${name}`}</p>      <button onClick={handleChangeName}>Click1</button>      <button onClick={click1}>Click2</button>    </div>;  }

這中方式已經實現了狀態整合,但是我們如果模擬一個state呢,來統一管理state呢,我們可以這麼實現

function App(props) {    let [state, setState] = useState({      name: 'dqhan'    });    let handleChangeName = useCallback(() => {      setState(preState => {        let updatedValues = {          name: '我變了'        }        return { ...preState, ...updatedValues }      })    })    return <div>      <p>{`hello~${state.name}`}</p>      <button onClick={handleChangeName}>Click</button>    </div>;  }

到目前為止,已經知道了react hook中如何使用state,那麼周期函數呢,那麼就涉及另一個鉤子useEffect

2.useEffect

作用:周期函數

useEffect涉及三個周期函數 componentDidMount 、componentDidUpdate、 compinentWillUmount 我們看一個簡單的實例

function App() {    var [count, setCount] = useState(0);    useEffect(() => {      console.log(`update--${count}`);    }, [count]);    return (      <div>        <button onClick={() => setCount(count + 1)}>Click</button>      </div>    );  }

下面我們來了解一下react hooks的周期函數,他是如何工作的
function App() {    let [count, setCount] = useState(0);    useEffect(      () => {        //默認每一次渲染都需要執行的方法        console.log('didmount')        //如果需要實現componentWillComponent,則return 一個函數即可            return function unmount() {          console.log('unmount')        }      }    )      let handleSetCount = useCallback(() => {      setCount((preCount) => {        let updatedCount = preCount + 1;        return updatedCount;      });    })      return <React.Fragment>      {console.log('render')}      <p>{`${count}`}</p>      <button onClick={handleSetCount}>Click</button>    </React.Fragment>  }

我們可以看一下執行周期

第一次渲染時候執行 render  didmount

點擊事件執行順序 render unmount didmount

不難看出,每一次渲染我們都會執行render進行渲染,然後清除掉上一次的useEffect,然後渲染完成之後重新執行useEffect

這樣通過一個useEffec可以默認執行兩個周期函數,也就是當我們需要對組件添加一些需要當組件卸載時候清除掉的功能時候,這個是很方便的,常見的就是setTimeout setIntrval等定時器

但是比如一個component渲染之後我們通常會發送一個請求來請求數據,然後重寫渲染這個組件,這樣會造成死循環怎麼辦,我們可以在useEffect後添加第二個參數

阻止useEffect每一次都要執行

function App() {    let [count, setCount] = useState(0);    useEffect(      () => {        //默認每一次渲染都需要執行的方法        console.log('didmount')        //如果需要實現componentWillComponent,則return 一個函數即可            return function unmount() {          console.log('unmount')        }      },      [setCount]    )      let handleSetCount = useCallback(() => {      setCount((preCount) => {        let updatedCount = preCount + 1;        return updatedCount;      });    })      return <React.Fragment>      {console.log('render')}      <p>{`${count}`}</p>      <button onClick={handleSetCount}>Click</button>    </React.Fragment>  }

當傳入第二個參數得值不變得時候就會跳過useEffect函數執行

如何模擬componentDidMount與componentWillUmount,第二個參數我們傳一個空數組,這樣就可以實現僅當組件渲染跟組件卸載得時候執行

function App() {    let [count, setCount] = useState(0);    useEffect(      () => {        //默認每一次渲染都需要執行的方法        console.log('didmount')        //如果需要實現componentWillComponent,則return 一個函數即可            return function unmount() {          console.log('unmount')        }      },      []    )      let handleSetCount = useCallback(() => {      setCount((preCount) => {        let updatedCount = preCount + 1;        return updatedCount;      });    })      return <React.Fragment>      {console.log('render')}      <p>{`${count}`}</p>      <button onClick={handleSetCount}>Click</button>    </React.Fragment>  }

不過,這隱藏了一個問題:傳遞空數組容易出現問題。如果咱們添加了依賴項,那麼很容易忘記向其中添加項,如果錯過了一個依賴項,那麼該值將在下一次運行useEffect時失效,並且可能會導致一些奇怪的問題。

常見得就是當我們想用父組件調用子組件時候使用得ref,或者我們要獲取dom焦點時

function App() {    let [count, setCount] = useState(0);    let [value, setValue] = useState('')    const inputRef = useRef();      useEffect(      () => {        //默認每一次渲染都需要執行的方法        console.log('didmount')          //如果需要實現componentWillComponent,則return 一個函數即可            return function unmount() {          console.log('unmount')        }      },      [inputRef]    )      let handleSetCount = useCallback(() => {      setCount((preCount) => {        let updatedCount = preCount + 1;        return updatedCount;      });    })      let handleSetValue = function (e) {      setValue(e.target.value);    }      return <React.Fragment>      {console.log('render')}      <p>{`${count}`}</p>      <input        ref={inputRef}        value={value}        onChange={handleSetValue}      ></input>      <button        ref={inputRef}        onClick={handleSetCount}      >Click</button>      </React.Fragment>  }

下面我們實現一個請求實例

請求實例

function App() {    let [data, setData] = useState(null);    useEffect(() => {      const fetchData = async () => {        const result = await axios(config);        setData(result);      };        fetchData();    }, []);      return <div></div>;  }

利用hook可以做到分離介面

function useFetchHook(config, watch) {    let [data, setData] = useState(null);    let [status, setStatus] = useState(0);    useEffect(      () => {        const fetchData = async () => {          try {            const result = await axios(config);            setData(result);            setStatus(0);          } catch (e) {            setStatus(1);          }          fetchData();        };      },      watch ? [...watch] : []    );      return { data, status };  }

現在我們整體上知道了useState useEffect怎麼使用了,我們來自己實現一個簡易的

實現useState

var val;  function useState(initVal) {    let resultVal = val || initVal;    function setVal(newVal) {      resultVal = newVal;      render();    }    return [resultVal, setVal]  }

實現useEffect

var watchArr = [];  function useEffect(fn, watch) {    var hasWatchChange = true;    hasWatchChange = watchArr && watch.every((val, i) => val === watchArr[i])    if (hasWatchChange) {      fn();      watchArr = watch;    }  }

hooks裡面最常用的兩個API就是useState與useEffect,現在是不是已經了解了呢,下面我們介紹一些其他API

3.useContext

作用:越級別獲取組件內容

類組件中我們也常用context,類組件實現方式


 const AppContext = React.createContext(‘target’);
class App extends React.Component {    constructor(props) {      super(props)    }    render() {      return (        <AppContext.Provider value="dark">          <Target />        </AppContext.Provider>      );    }  }    class Target extends React.Component {    //通過定義靜態屬性 contextType 來訂閱    //沒有定義是獲取不到的    static contextType = AppContext;    render() {      console.log(this.context);      return <div></div>;    }  }

Hooks實現方式

const AppContext = React.createContext('target');    function App() {    useEffect(      () => { },      []    );    return <AppContext.Provider value="dark">      <Target />    </AppContext.Provider>;  }    function Target() {    const value = useContext(AppContext);    console.log(value);    return <div></div>;  }

在需要訂閱多個 context 的時候,就更能體現出useContext的優勢。

傳統的實現方式

function App() {    return <CurrentUser.Consumer>      {        user => <Notifications.Consumer>          {notifications =>            <header>              Welcome back, {user.name}!                You have {notifications.length} notifications.            </header>          }        </Notifications.Consumer>      }    </CurrentUser.Consumer>  }

hooks實現

function App() {    const user = useContext(CurrentUser);    const notifications = useContext(Notifications);      return (      <header>        Welcome back, {user.name}!        You have {notifications.length} notifications.      </header>    );  }

是不是比傳統的要簡單的多

4.useReducer

作用:複雜狀態管理,跟redux本質上是一樣的

函數式組件如果涉及到狀態管理,我們需要藉助redux,那麼hooks需要嗎,答案也是一樣的,簡單的狀態管理我們可以通過useState來進行管理,如果比較複雜的狀態管理呢,react hook給我們提供了方法 useReducer

function init(initialCount) {    return { count: initialCount };  }    function reducer(state, action) {    switch (action.type) {      case 'increment':        return { count: state.count + 1 };      case 'decrement':        return { count: state.count - 1 };      case 'reset':        return init(action.payload);      default:        throw new Error();    }  }    function Counter({ initialCount }) {    const [state, dispatch] = useReducer(reducer, initialCount, init);    return (      <>        Count: {state.count}        <button          onClick={() => dispatch({ type: 'reset', payload: initialCount })}>          Reset        </button>        <button onClick={() => dispatch({ type: 'increment' })}>+</button>        <button onClick={() => dispatch({ type: 'decrement' })}>-</button>      </>    );  }

5.useCallback

作用:提升性能,快取事件,減少沒必要的渲染

當我們使用類組件創建時,我們會怎麼綁定事件呢

class App extends React.Component {    constructor(props) {      super(props);    }    render() {      return <div>        <p>{`hello~${name}`}</p>        <button onClick={() => { console.log('click') }}>Click</button>      </div>    }  }

這樣寫會導致什麼結果呢,就是當渲染的時候react會認為每一次綁定的事件都是新的,從而從新進行計算

改進如下

class App extends React.Component {    constructor(props) {      super(props);      this.handleClick = this.handleClick.bind(this);    }    handleClick() {      console.log('click')    }    render() {      return <div>        <p>{`hello~${name}`}</p>        <button onClick={this.handleClick}>Click</button>      </div>    }  }

我們講觸發函數綁定在this上,來快取這個方法

hooks

function App() {    let [count, setCount] = useState(0);    return <div>      <button onClick={() => setCount(1)} ></button>    </div>  }

同樣的問題這麼寫也是存在的,改進如下

function App() {    let [count, setCount] = useState(0);    let handleSetCount = useCallback(() => {      setCount(1);    })    return <div>      <button onClick={handleSetCount} ></button>    </div>  }

我們通過useCallback來快取這個事件達到優化效果

6.useMemo

作用:提升性能,選擇性的渲染變化組件

function App(target, target2) {    const target = useMemo(() => {      return <Target />    }, [target])    const target2 = useMemo(() => {      return <Target2 />    }, [target2])    return <div>      {target}      {target2}    </div>  }

當target變化僅渲染Target組件,同理也作用與Target2組件

React.memo

作用:提升性能

如果想實現class中的shouldComponentUpdate方法呢 ,區別是它只能比較 props,不會比較 state:

const App = React.mome((target, target2) => {    const target = useMemo(() => {      return <Target />    }, [target])    const target2 = useMemo(() => {      return <Target2 />    }, [target2])    return <div>      {target}      {target2}    </div>  })

7.useRef

作用:獲取dom依賴關係

類組件實現方式

class App extends React.Component {    constructor(props) {      super(props);      this.myRef = React.createRef();    }      componentDidMount() {      this.myRef.current.focus();    }      render() {      return <input ref={this.myRef} type="text" />;    }  }

hooks

function App() {    let [value, setValue] = useState('')    const inputRef = useRef();      useEffect(      () => {      },      [inputRef]    )      let handleSetValue = function (e) {      setValue(e.target.value);    }      return <React.Fragment>      <input        ref={inputRef}        value={value}        onChange={handleSetValue}      ></input>      </React.Fragment>  }

好了hooks的基本使用方式就介紹完了,現在你對hook多少能了解了一些吧~

程式碼地址:https://github.com/Dqhan/React