React Hooks 溫故而知新

React Hooks 是 React 16.8 新增的特性,它可讓你在不編寫 class 的狀況下使用 state 以及其它的 React 特性。html

首先來看一下使用 Hooks 編寫的代碼是什麼樣子的:react

import React, { useState, useEffect } from ‘react’;

export default () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = count;
  });

  return (
    <div> <p>Current count is: {count}</p> <button onClick={() => setCount(count + 1)}>increment count</button> </div>
  );
};
複製代碼

這段代碼實現了一個簡單的計數器功能,點擊按鈕使 count 增長,同時使頁面標題的顯示與 count 的變化同步。這裏引入了 useStateuseEffect,咱們稍後再來介紹他們,先來對比一下與 class component 的區別:數組

import React, { Component } from ‘react’;

export default class extends Component {
  state = {
    count: 0,
  };

  componentDidMount() {
    document.title = this.state.count;
  }

  componentDidUpdate() {
    document.title = this.state.count;
  }

  render() {
    return (
      <div> <p>Current count is: {this.state.count}</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> increment count </button> </div>
    );
  }
}
複製代碼

經過對比發現使用 Hooks 編寫代碼簡練多了,state 初始化及變動 state 的方式發生了改變,生命週期函數也不用再編寫了,Hooks 使咱們擁有了在函數組件裏使用 state 及生命週期的能力。React 的組件分類也進行了從新定義,之前是有狀態組件、無狀態組件,如今劃分爲了類組件及函數組件。瀏覽器

那麼接下來就來詳細分析一下 Hooks 的具體用法。性能優化

useState

首先是 useState,這個是幹什麼用的?useState 可讓咱們在函數組件內部使用 state 進行狀態管理,語法以下:ide

// state, 和 setState 能夠任意命名,好比 count, setCount
const [state, setState] = useState(initialState)
複製代碼

useState 接收一個初始的狀態 initialState,它返回一個數組,進行解構後一個是 state,一個是更新 state 的函數 setState,每次 setState 調用後,就會將組件的一次從新渲染加入隊列(從新渲染組件或者和其它的 setState 進行批量更新)。函數

initialState 做爲初始狀態,能夠是基本類型,也能夠是對象(例如{a:1})或者函數,它只在組件首次渲染時被用到,首次渲染後 state 與 initialState 相同。佈局

在每次從新渲染後,useState 都會返回最新的 state。而 setState 就至關因而類組件中的 this.setState(),可是 Hooks 裏的 setState 並不會進行新 state 與舊 state 的合併,而是直接覆蓋。性能

另外 useState() 接收的參數也是任意的,能夠是基本類型,也能夠是對象,還能夠傳入一個函數,在這個函數裏能夠拿到舊的 state,例如:學習

{/* <button onClick={() => setCount(count + 1)}>increment count</button> */}
<button onClick={() => setCount((oldCount) => oldCount + 1)}>
  increment count
</button>
複製代碼

useState 也可使用屢次,進行多個狀態管理:

const [count, setCount] = useState(0);
const [color, setColor] = useState('red');
複製代碼

useEffect

那麼 useEffect 又是幹什麼的呢?useEffect 可讓咱們在函數組件中進行反作用操做,好比獲取數據、DOM 操做、日誌記錄等都屬於反作用。

useEffect 能夠看作是 componentDidMount, componentDidUpdate, componentWillUnmount 這三個生命週期函數的組合。

例如在前面的例子中咱們使用 useEffect 來更新頁面標題:

useEffect(() => {
  document.title = count;
});
複製代碼

每次使用 setCount 更新狀態,useEffect 裏的函數都執行了一次,這就至關因而在類組件中同時使用了 componentDidMountcomponentDidUpdate

componentDidMount() {
  document.title = this.state.count;
}
componentDidUpdate() {
  document.title = this.state.count;
}
複製代碼

能夠看到,在兩個生命週期裏寫了一樣的邏輯,這無疑形成了代碼的冗餘,而 useEffect 則會在每次渲染以後都執行,包括首次渲染,這樣咱們把相似於示例中的相同操做編寫一次放在 useEffect 中便可。

對於上面示例中的反作用是無需清除的,可是還有些反作用是須要清除的,例如手動綁定的 DOM 事件。

在類組件中清除反作用能夠在 componentWillUnmount 生命週期方法中進行,那麼在 Hooks 裏該如何實現呢?

其實,useEffect 已經提供了清除的機制,每一個反作用均可以添加一個可選的返回函數,用來在組件卸載的時候進行清除操做,好比下面代碼:

import React, { useState, useEffect } from ‘react’;

export default () => {
  const [size, setSize] = useState({ width: 0, height: 0 });

  const handleResize = () => {
    const win = document.documentElement.getBoundingClientRect();
    setSize({ width: win.width, height: win.height });
  };

  useEffect(() => {
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  });

  return (
    <div> <p>Current size is:</p> <p> width: {size.width}, height: {size.height} </p> </div>
  );
};
複製代碼

另外使用 useEffect 還有一個問題,就是每次進行 setState 操做時,可能都會觸發反作用的執行, 即便更改的 state 和這個反作用並無關係,好比下面的代碼:

import React, { useState, useEffect } from ‘react’;

export default () => {
  const [count, setCount] = useState(0);
  const [color, setColor] = useState('red');

  useEffect(() => {
    console.log('count');
    document.title = count;
  });

  return (
    <div> <p style={{ color: color }}>Current count is: {count}</p> <button onClick={() => setCount(count + 1)}>increment count</button> <br /> <button onClick={() => setColor(color === ‘red’ ? ‘blue’ : ‘red’)}> switch color </button> </div>
  );
};
複製代碼

咱們發如今改變顏色時,修改頁面標題的反作用也在反覆執行,這無疑形成了資源的浪費,更不是咱們想要的效果。固然,useEffect 給咱們提供了第二個參數,能夠對渲染進行控制,它接收一個數組做爲參數,能夠傳入多個值,咱們來對上面代碼進行改進:

useEffect(() => {
  console.log('count');
  document.title = count;
}, [count]);
複製代碼

咱們傳入了 [count] 做爲第二個參數,這樣在每次渲染時都會先對 count 的新值和舊值進行對比,只有變化的時候這個反作用纔會執行。

另外使用 Hooks 還須要遵循以下規則:

  • 只在函數組件中使用 Hooks,不要在普通函數中使用
  • 只在最頂層使用 Hooks,不要在條件、循環或者嵌套函數中使用 Hooks

固然爲了保證這些規則,咱們可使用 ESLint 插件 eslint-plugin-react-hooks 進行約束。

useContext

首先看一下 Context 是怎麼幹什麼用的:Context 提供了一種跨層級傳遞數據的方式,有了它就無需在每層組件都手動傳遞 props 了。

Context API 使用示例

經過下面例子回顧一下 Context API 的使用:

// 建立一個 Context 對象
const ThemeContext = React.createContext(‘light’);

const ContextAPIDemo = () => {
  return (
    // 每一個 Context 對象都會返回一個 Provider React 組件,
    // 它接收一個 value 屬性,傳遞給消費組件,
    // 容許消費組件訂閱 context 的變化
    <ThemeContext.Provider value="dark">
      <MiddleComponent />
    </ThemeContext.Provider>
  );
};

// 一箇中間組件,使用 Context 傳遞數據並不須要中間組件透傳
const MiddleComponent = () => {
  return (
    <div>
      <ThemedButton />
    </div>
  );
};

class ThemedButton extends Component {
	// 把建立的 ThemeContext 對象賦值給 contextType 靜態屬性,
	// 這樣就能夠經過 this.context 訪問到 ThemeContext 裏面的數據
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

const Button = (props) => {
  const btnBgColor = props.theme === ‘dark’ ? ‘#333’ : ‘white’;
  return <button style={{ backgroundColor: btnBgColor }}>Toggle Theme</button>;
};
複製代碼

這裏使用 React.createContext() 建立了一個 ThemeContext,而後經過 Provider 把數據傳遞給消費組件,即 ThemedButton,最後在消費組件裏把建立的 ThemeContext 賦值給了 contextType 靜態屬性,這樣咱們在消費組件裏就能夠經過 this.context 的方式訪問數據了。

固然在消費組件裏還可使用 Consumer來獲取 Context 的數據:

const ThemedButton = () => {
  return (
    <ThemeContext.Consumer>
      {(theme) => <Button theme={theme} />}
    </ThemeContext.Consumer>
  );
};
複製代碼

Ok,作了簡單的回顧以後,就來看一下在 Hooks 裏該如何使用 Context。

useContext 的使用

接下來咱們使用 useContext 改造下上面的示例:

// 省略其它代碼…
const Button = () => {
  const theme = useContext(ThemeContext);
  const btnBgColor = theme === ‘dark’ ? ‘#333’ : ‘white’;
  return <button style={{ backgroundColor: btnBgColor }}>Toggle Theme</button>;
};
複製代碼

改造後就徹底能夠不用 ConsumercontextType 靜態屬性了,哪裏須要哪裏就直接使用 useContext 便可。

useContext 接收一個 Context 對象做爲參數,返回的是經過 Provider 傳入的 value 數據。

因爲示例中一直是一個固定的 ThemeContet,若是須要一個動態的 Context 該怎麼辦?我麼能夠經過 Providervalue 傳入回調函數進行處理:

const ContextAPIDemo = () => {
  const [theme, setTheme] = useState(initialState.theme);

  const toggleTheme = (val) => {
    setTheme(val === 'dark' ? 'light' : 'dark');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}> <MiddleComponent /> </ThemeContext.Provider> ); }; 複製代碼

而後在目標組件能夠經過 useContext 拿到回調函數,例如:

const Button = () => {
  const { theme, toggleTheme } = useContext(ThemeContext);
  const btnBgColor = theme === ‘dark’ ? ‘#333’ : ‘white’;
  return (
    <button
      style={{ backgroundColor: btnBgColor }}
      onClick={() => toggleTheme(theme)}
    >
      Toggle Theme
    </button>
  );
};
複製代碼

useReducer

提起 Reducer,用過 Redux 的同窗應該都不陌生,Reducer Hook 裏的 Reducer 其實跟 Redux 裏的 Reducer 是同一個意思,接收舊的 state,返回新的 state,形如:(state, action) => newState

useReducer 的基本語法是這樣的:

const [state, dispatch] = useReducer(reducer, initialArg, init);
複製代碼

它接收三個參數:reducer, initialArg 及可選的 init函數:

  • reducer: 接收舊的 state,返回新的 state,形如:`(state, action) => newState
  • initialArg: 初始 state
  • init: 做爲一個函數傳入,惰性地初始化 state,state 將會被設置爲 init(initialArg),這樣能夠將計算 state 的邏輯提取到外部,同時若是有重置 state 的需求的話也會很方便

下面就來看一下具體的用法:

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();
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <> Count: {state.count} <button onClick={() => dispatch({ type: 'decrement' })}>-</button> <button onClick={() => dispatch({ type: ‘increment’ })}>+</button> </> ); }; 複製代碼

useMemo

useMemo 的語法是這樣的:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製代碼

接收一個函數和依賴數組做爲參數,而後只在依賴項變化時纔會計算 memoizedValue 的值,基於這一點,咱們能夠做爲性能優化的一個手段。

例以下面代碼,咱們經過對 computedNum 這個計算方法添加了 useMemo,把 count 做爲依賴,只有 count 變化時 computedNum 纔會計算,這樣能夠防止無關的操做也會引發函數的執行(若是不加,在點擊 change color 時,computedNum也會反覆計算)。

export default () => {
  const [count, setCount] = useState(0);
  const [color, setColor] = useState(‘blue’);

  const computedNum = useMemo(() => {
    console.log('render when count change');
    return count + 1;
  }, [count]);

  return (
    <div> <p>Current color is: {color}</p> <p>Current count is: {count}</p> <p>Computed num is: {computedNum}</p> <button onClick={() => setCount(count + 1)}>increment count</button> <button onClick={() => setColor(color === ‘blue’ ? ‘green’ : ‘blue’)}> change color </button> </div>
  );
};
複製代碼

固然,咱們也能夠用 useMemo 去包裹一個組件,從而實現相似 PureComponent 的效果,防止組件反覆的渲染。

<>
  {useMemo(
    () => (
      <ColorDemo color={color} /> ), [color] )} <> 複製代碼

useCallback

useCallback 用法以下:

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
複製代碼

useCallback 返回了一個 memoized 回調函數,它跟 useMemo 相似,useCallback(fn, deps) 至關於 useMemo(() => fn, deps)。它的第二個參數跟 useMemo 的同樣,傳入一個依賴項數組,只有依賴變化時纔會生成新的函數,不然一直是同一個函數,這在進行組件性能優化時很是有用,避免多餘的開銷。好比使用 props 傳遞函數時,使用 callback 能夠避免每次渲染都生成一個新的函數,示例以下:

let firstOnClick;
const Button = (props) => {
  if (!firstOnClick) {
    firstOnClick = props.onClick;
  }

  console.log(firstOnClick === props.onClick);
  return <button onClick={props.onClick}>increment count</button>;
};

export default () => {
  const [count, setCount] = useState(0);
  const [color, setColor] = useState('blue');

  // 不用 useCallback 包裹的話,點擊 change color 按鈕,Button 組件裏每次也都會生成新的函數
  // const handleIncrementCount = () => {
  // setCount(count + 1);
  // };

  // 使用 useCallback 包裹後,只有點擊 increment count 時 count 發生變化,纔會生成新的函數,
  // 修改顏色並不會生成新函數
  const handleIncrementCount = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div> <p>Current color is: {color}</p> <p>Current count is: {count}</p> <Button onClick={handleIncrementCount} /> <button onClick={() => setColor(color === 'blue' ? 'green' : 'blue')}> change color </button> </div> ); }; 複製代碼

useRef

useRef 會返回一個可變的 ref 對象,其 .current 屬性被初始化爲傳入的參數(initialValue),返回的 ref 對象在組件的整個生命週期內保持不變。

一般它被用來獲取子組件(或 DOM 元素):

export default () => {
  const btnRef = useRef(null);

  useEffect(() => {
    btnRef.current.addEventListener('click', () => {
      alert('click me');
    });
  }, []);

  return (
    <div> <button ref={btnRef}>click</button> </div>
  );
};
複製代碼

另外,因爲 useRef 在每次渲染時返回的都是同一個 ref 對象,所以能夠用 ref.current 保存一個變量,它不會隨着組件的從新渲染而受到影響,例以下面一個定時器的例子:

export default () => {
  const [count, setCount] = useState(0);
  const ref = useRef(0);

  useEffect(() => {
    const timer = setInterval(() => {
      console.log('interval');
      setCount(++ref.current);
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return (
    <div> <p>Current count is: {count}</p> </div>
  );
};
複製代碼

同時,變動 .current 屬性也不會引發組件的從新渲染。

useImperativeHandle

useImperativeHandle 可讓咱們在使用 ref 時自定義暴露給父組件的實例值,這樣能夠隱藏掉一些私有方法或屬性,下面是一個 useImperativeHandleforwardRef 配合使用的例子:

function Input(props, ref) {
  const [val, setVal] = useState(0);

  useEffect(() => {
    setVal(props.count);
  }, [props.count]);

  const clearInput = useCallback(() => {
    setVal('');
  }, []);

  useImperativeHandle(ref, () => ({
    clear: () => {
      clearInput();
    },
  }));

  return (
    <input type="text" value={val} onChange={(e) => setVal(e.target.value)} />
  );
}

const FancyInput = forwardRef(Input);

export default () => {
  const [count, setCount] = useState(0);
  const fancyRef = useRef(null);

  const handleClearInput = useCallback(() => {
    fancyRef.current.clear();
  }, []);

  return (
    <div>
      <p>Current count is: {count}</p>
      <button onClick={() => setCount(count + 1)}>increment count</button>
      <hr />
      <FancyInput ref={fancyRef} count={count} />
      <button onClick={handleClearInput}>clear input</button>
    </div>
  );
};
複製代碼

useLayoutEffect

useLayoutEffectuseEffect 用法相同,只不過 useLayoutEffect 會在全部 DOM 更新以後同步調用,可使用它讀取 DOM 佈局並同步觸發重渲染,可是仍是建議使用 useEffect,以免阻塞 UI 更新。

這裏有個示例,使用useEffectuseLayoutEffect 會有明顯的差別:

const useLayoutEffectDemo = () => {
  const [height, setHeight] = useState(100);
  const boxRef = useRef(null);

  useLayoutEffect(() => {
    if (boxRef.current.getBoundingClientRect().height < 200) {
      console.log('set height: ', height);
      setHeight(height + 10);
    }
  }, [height]);

  const style = {
    width: '200px',
    height: `${height}px`,
    backgroundColor: height < 200 ? 'red' : 'blue',
  };

  return (
    <div ref={boxRef} style={style}> useLayoutEffect Demo </div>
  );
};
複製代碼

咱們至關於給盒子設置了粗糙的過渡變化,使用 useEffect 這種過渡是生效的,可是換成 useLayoutEffect 以後,過渡效果已經沒了,只會顯示最終的效果。也就是說在瀏覽器執行繪製以前,useLayoutEffect 內部的更新計劃將被同步刷新。

useDebugValue

用於開發自定義 Hooks 調試使用,例如:

useDebugValue(
  size.width < 500 ? '---- size.width < 500' : '---- size.width > 500'
);
複製代碼

能夠在 React DevTools 裏查看相關的信息輸出。

自定義 Hooks

React Hooks 的強大,不只僅是由於官方的內置 Hooks,同時它還支持自定義 Hooks,提升了組件的複用性,從必定程度了取代了 HOC 和 render props 的複用方式。

自定義 Hook 須要以 use 開頭,其內部還能夠調用其餘的 Hook,自定義 Hook 不須要具備特殊的標識,咱們能夠自定義參數及返回值,下面是一個自定義 Hook 示例,自定了一個 useSize,用來獲取窗口的大小。

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

const getDomElInfo = () => document.documentElement.getBoundingClientRect();

const getSize = () => ({
  width: getDomElInfo().width,
  height: getDomElInfo().height,
});

export default function useSize() {
  const [size, setSize] = useState(() => getSize());

  const onChange = useCallback(() => {
    setSize(() => getSize());
  }, []);

  useEffect(() => {
    window.addEventListener('resize', onChange, false);

    return () => {
      window.removeEventListener('resize', onChange, false);
    };
  }, [onChange]);

  return size;
}
複製代碼

使用這個自定義 Hook 也很簡單:

import useSize from './useSize';

export default () => {
  const size = useSize();
  return (
    <div> <p> size: width-{size.width}, height-{size.height} </p> </div>
  );
};
複製代碼

好了,關於 React Hooks 就介紹到這裏,它不只僅打破了原有的組件編寫方式,更是一種新的思惟, 很是值得學習。

相關參考


相關文章
相關標籤/搜索