React Hooks不徹底解讀

hooks.jpg

什麼是hooks?

hooks 是 react 在16.8版本開始引入的一個新功能,它擴展了函數組件的功能,使得函數組件也能實現狀態、生命週期等複雜邏輯。javascript

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

上面是 react 官方提供的 hooks 示例,使用了內置hookuseState,對應到<u>Class Component</u>應該這麼實現html

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

簡而言之,hooks 就是鉤子,讓你能更方便地使用react相關功能。java

hooks解決了什麼問題?

看完上面一段,你可能會以爲除了代碼塊精簡了點,沒看出什麼好處。別急,繼續往下看。react

過去,咱們習慣於使用<u>Class Component</u>,可是它存在幾個問題:git

  • 狀態邏輯複用困難github

    • 組件的狀態相關的邏輯一般會耦合在組件的實現中,若是另外一個組件須要相同的狀態邏輯,只能藉助<u>render props</u> 和 <u>high-order components</u>,然而這會破壞原有的組件結構,帶來 JSX wrapper hell 問題。
  • side effect 複用和組織困難json

    • 咱們常常會在組件中作一些有 side effect 的操做,好比請求、定時器、打點、監聽等,代碼組織方式以下
    class FriendStatusWithCounter extends React.Component {
      constructor(props) {
        super(props);
        this.state = { count: 0, isOnline: null };
        this.handleStatusChange = this.handleStatusChange.bind(this);
      }
    
      componentDidMount() {
        document.title = `You clicked ${this.state.count} times`;
        ChatAPI.subscribeToFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
      }
    
      componentDidUpdate() {
        document.title = `You clicked ${this.state.count} times`;
      }
    
      componentWillUnmount() {
        ChatAPI.unsubscribeFromFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
      }
    
      handleStatusChange(status) {
        this.setState({
          isOnline: status.isOnline
        });
      }
      
      render() {
        return (
          <div>
            <p>You clicked {this.state.count} times</p>
            <p>Friend {this.props.friend.id} status: {this.state.isOnline}</p>
            <button onClick={() => this.setState({ count: this.state.count + 1 })}>
              Click me
            </button>
          </div>
        );
      }
    }

複用的問題就不說了,跟狀態邏輯同樣,主要說下代碼組織的問題。1. 爲了在組件刷新的時候更新文檔的標題,咱們在componentDidMountcomponentDidUpdate中各寫了一遍更新邏輯; 2. 綁定朋友狀態更新和解綁的邏輯,分散在componentDidMountcomponentWillUnmount中,實際上這是一對有關聯的邏輯,若是能寫在一塊兒最好;3. componentDidMount中包含了更新文檔標題和綁定事件監聽,這2個操做自己沒有關聯,若是能分開到不一樣的代碼塊中更利於維護。redux

  • Javascript Class 天生缺陷數組

    • 開發者須要理解this的指向問題,須要記得手動 bind 事件處理函數,這樣代碼看起來很繁瑣,除非引入@babel/plugin-proposal-class-properties(這個提案目前還不穩定)。
    • 現代工具沒法很好地壓縮 class 代碼,致使代碼體積偏大,hot reloading效果也不太穩定。

爲了解決上述問題,hooks 應運而生。讓咱們使用 hooks 改造下上面的例子promise

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

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  return isOnline;
}

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  const isOnline = useFriendStatus(props.friend.id);
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <p>Friend {props.friend.id} status: {isOnline}</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

function FriendStatus(props) {
  // 經過自定義hook複用邏輯
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

看,問題都解決了!

怎麼使用?

hooks 通常配合<u>Function Components</u>使用,也能夠在內置 hooks 的基礎上封裝自定義 hook。

先介紹下 react 提供的內置 hooks。

useState

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

useState接收一個參數做爲初始值,返回一個數組,數組的第一個元素是表示當前狀態值的變量,第二個參數是修改狀態的函數,執行的操做相似於this.setState({ count: someValue }),固然內部的實現並不是如此,這裏僅爲了幫助理解。

useState能夠屢次調用,每次當你須要聲明一個state時,就調用一次。

function ExampleWithManyStates() {
  // Declare multiple state variables!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

須要更新某個具體狀態時,調用對應的 setXXX 函數便可。

useEffect

useEffect的做用是讓你在<u>Function Components</u>裏面能夠執行一些 side effects,好比設置監聽、操做dom、定時器、請求等。

  • 普通side effect
useEffect(() => {
  document.title = `You clicked ${count} times`;
});
  • 須要清理的effect,回調函數的返回值做爲清理函數
useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  // Specify how to clean up after this effect:
  return function cleanup() {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
});

須要注意,上面這種寫法,每次組件更新都會執行 effect 的回調函數和清理函數,順序以下:

// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // Run first effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // Run next effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // Run next effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect

這個效果等同於在componentDidMountcomponentDidUpdatecomponentWillUnmount實現了事件綁定和解綁。若是隻是組件的 state 變化致使從新渲染,一樣會從新調用 cleanup 和 effect,這時候就顯得沒有必要了,因此 useEffect 支持用第2個參數來聲明依賴

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]);

第2個參數是一個數組,在數組中傳入依賴的 state 或者 props,若是依賴沒有更新,就不會從新執行 cleanup 和 effect。

若是你須要的是隻在初次渲染的時候執行一次 effect,組件卸載的時候執行一次 cleanup,那麼能夠傳一個空數組[]做爲依賴。

useContext

context這個概念你們應該不陌生,通常用於比較簡單的共享數據的場景。useContext就是用於實現context功能的 hook。

來看下官方提供的示例

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);

  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

代碼挺長,可是一眼就能看懂了。把 context 對象傳入useContext,就能夠拿到最新的 context value。

須要注意的是,只要使用了useContext的組件,在 context value 改變後,必定會觸發組件的更新,哪怕他使用了React.memo或是shouldComponentUpdate

useReducer

useReducer(reducer, initialArg)返回[state, dispatch],跟 redux 很像。

const initialState = {count: 0};

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 Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

除此以外,react 內置的 hooks 還包括useCallbackuseMemouseRefuseImperativeHandleuseLayoutEffectuseDebugValue,這裏就再也不贅述了,能夠直接參考官方文檔

自定義 hook

基於內置 hook,咱們能夠封裝自定義的 hook,上面的示例中已經出現過useFriendStatus這樣的自定義 hook,它能幫咱們抽離公共的組件邏輯,方便複用。注意,自定義 hook 也須要以use開頭。

咱們能夠根據須要建立各類場景的自定義 hook,如表單處理、計時器等。後面實戰場景的章節中我會具體介紹幾個例子。

實現原理

hooks 的使用須要遵循幾個規則:

  • 必須在頂層調用,不能包裹在條件判斷、循環等邏輯中
  • 必須在 <u>Function Components</u> 或者自定義 hook 中調用

之因此有這些規則限制,是跟 hooks 的實現原理有關。

這裏咱們嘗試實現一個簡單的版本的useStateuseEffect用來講明。

const memoHooks = [];
let cursor = 0;

function useState(initialValue) {
  const current = cursor;
  const state = memoHooks[current] || initialValue;
  function setState(val) {
    memoHooks[current] = val;
    // 執行re-render操做
  }
  cursor++;
  return [state, setState];
}

function useEffect(cb, deps) {
  const hasDep = !!deps;
  const currentDeps = memoHooks[cursor];
  const hasChanged = currentDeps ? !deps.every((val, i) => val === currentDeps[i]) : true;
  if (!hasDep || hasChanged) {
    cb();
    memoHooks[cursor] = deps;
  }
  cursor++;
}

此時咱們須要構造一個函數組件來使用這2個 hooks

function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]);
  const [name, setName] = useState('Joe');
  useEffect(() => {
    console.log(`Your name is ${name}`);
  });
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
  1. 渲染前:memoHooks 爲[],cursor 爲0
  2. 第一次渲染

    1. 執行const [count, setCount] = useState(0);,memoHooks 爲[0],cursor 爲0
    2. 執行useEffect(() => { document.title = You clicked ${count} times; }, [count]);,memoHooks 爲[0, [0]],cursor 爲1
    3. 執行const [name, setName] = useState('Joe');,memoHooks 爲[0, [0], 'Joe'],cursor 爲2
    4. 執行useEffect(() => { console.log(Your name is ${name}); });,memoHooks 爲[0, [0], 'Joe', undefined],cursor 爲3
  3. 點擊按鈕

    1. 執行setCount(count + 1),memoHooks 爲[1, [0], 'Joe', undefined],cursor 爲0
    2. 執行 re-render
  4. re-render

    1. 執行const [count, setCount] = useState(0);,memoHooks 爲[1, [0], 'Joe', undefined],cursor 爲0
    2. 執行useEffect(() => { document.title = You clicked ${count} times; }, [count]);,memoHooks 爲[1, [1], 'Joe', undefined],cursor 爲1。這裏因爲hooks[1]的值變化,會致使 cb 再次執行。
    3. 執行const [name, setName] = useState('Joe');,memoHooks 爲[1, [1], 'Joe', undefined],cursor 爲2
    4. 執行useEffect(() => { console.log(Your name is ${name}); });,memoHooks 爲[1, [1], 'Joe', undefined],cursor 爲3。這裏因爲依賴爲 undefined,致使 cb 再次執行。

經過上述示例,應該能夠解答爲何 hooks 要有這樣的使用規則了。

  • 必須在頂層調用,不能包裹在條件判斷、循環等邏輯中:hooks 的執行對於順序有強依賴,必需要保證每次渲染組件調用的 hooks 順序一致。
  • 必須在 <u>Function Components</u> 或者自定義 hook 中調用:不論是內置 hook,仍是自定義 hook,最終都須要在 <u>Function Components</u> 中調用,由於內部的memoHookscursor其實都跟當前渲染的組件實例綁定,脫離了<u>Function Components</u>,hooks 也沒法正確執行。

固然,這些只是爲了方便理解作的一個簡單demo,react 內部其實是經過一個單向鏈表來實現,並不是 array,有興趣能夠自行翻閱源碼。

實戰場景

操做表單

實現一個hook,支持自動獲取輸入框的內容。

function useInput(initial) {
  const [value, setValue] = useState(initial);
  const onChange = useCallback(function(event) {
    setValue(event.currentTarget.value);
  }, []);
  return {
    value,
    onChange
  };
}

// 使用示例
function Example() {
  const inputProps = useInput('Joe');
  return <input {...inputProps} />
}

網絡請求

實現一個網絡請求hook,可以支持初次渲染後自動發請求,也能夠手動請求。參數傳入一個請求函數便可。

function useRequest(reqFn) {
  const initialStatus = {
    loading: true,
    result: null,
    err: null
  };
  const [status, setStatus] = useState(initialStatus);
  function run() {
    reqFn().then(result => {
      setStatus({
        loading: false,
        result,
        err: null
      })
    }).catch(err => {
      setStatus({
        loading: false,
        result: null,
        err
      });
    });
  }
  // didMount後執行一次
  useEffect(run, []);
  return {
    ...status,
    run
  };
}

// 使用示例
function req() {
  // 發送請求,返回promise
  return fetch('http://example.com/movies.json');
}
function Example() {
  const {
    loading,
    result,
    err,
    run
  } = useRequest(req);
  return (
    <div>
      <p>
        The result is {loading ? 'loading' : JSON.stringify(result || err)}
      </p>
      <button onClick={run}>Reload</button>
    </div>
  );
}

上面2個例子只是實戰場景中很小的一部分,卻足以看出 hooks 的強大,當咱們有豐富的封裝好的 hooks 時,業務邏輯代碼會變得很簡潔。推薦一個github repo,這裏羅列了不少社區產出的 hooks lib,有須要自取。

使用建議

根據官方的說法,在可見的將來 react team 並不會中止對 class component 的支持,由於如今絕大多數 react 組件都是以 class 形式存在的,要所有改造並不現實,並且 hooks 目前還不能徹底取代 class,好比getSnapshotBeforeUpdatecomponentDidCatch這2個生命週期,hooks尚未對等的實現辦法。建議你們能夠在新開發的組件中嘗試使用 hooks。若是通過長時間的迭代後 function components + hooks 成爲主流,且 hooks 從功能上能夠徹底替代 class,那麼 react team 應該就能夠考慮把 class component 移除,畢竟沒有必要維護2套實現,這樣不只增長了維護成本,對開發者來講也多一份學習負擔。

參考文章

相關文章
相關標籤/搜索