萬字總結,React Hooks 初探

「這是我參與8月更文挑戰的第6天,活動詳情查看: 8月更文挑戰javascript

1. React Hooks誕生以前

Hook 是 React 16.8 的新增特性,它可讓咱們在不編寫class的狀況下使用state以及其餘的React特性(好比生命週期)。React Hooks 的出現是對類組件函數組件這兩種組件形式的思考和側重。下面就來看看函數組件和類組件分別有哪些優缺點。java

(1)類組件

類組件是基於 ES6中的 Class 寫法,經過繼承 React.Component 得來的 React 組件。下面是一個類組件:react

import React from 'react';

class ClassComponent extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      text: ""
    }
  }
  
  componentDidMount() {
    //...
  }
  changeText = (newText) => {
    this.setState({
      text: newText
    });
  };

  render() {
    return (
      <div> <p>{this.state.text}</p> <button onClick={this.changeText}>修改</button> </div>
    );
  }
}

export default ClassComponent
複製代碼

對於類組件,總結其優勢以下:web

  • 組件狀態: 類組件能夠定義本身的state,用來保存組件內部狀態;而函數組件不能夠,函數每次調用都會產生新的臨時變量;
  • 生命週期: 類組件有生命週期,能夠在對應的生命週期中完成業務邏輯,好比在componentDidMount中發送網絡請求,而且該生命週期函數只會執行一次;而在函數組件中發送網絡請求時,每次從新渲染都會從新發送一次網絡請求;
  • 渲染優化: 類組件能夠在狀態改變時只從新執行render函數以及但願從新調用的生命週期函數componentDidUpdate等;而函數組件在從新渲染時,整個函數都會被執行。

對於類組件,總結其缺點以下:npm

  • 難以拆分: 隨着業務的增多,類組件會變得愈來愈複雜,不少邏輯每每混在一塊兒,強行拆分反而會形成過分設計,增長了代碼的複雜度;
  • 難以理解:類組件中有 this 生命週期這兩大痛點。對於生命週期,不只學習成本高,而且須要將業務邏輯規劃在合適的生命週期中,每一個生命週期中的邏輯看上去毫無關聯,邏輯就像是被「打散」進生命週期裏了同樣;除此以外,在類組件中涉及到了 this 的指向,咱們必須搞清楚this的指向究竟是誰,這個過程就很容易出現問題。爲了解決 this 不符合預期的問題,可使用 bind、箭頭函數來解決。但本質上都是在用實踐層面的約束來解決設計層面的問題
  • 難以複用組件狀態: 複用狀態邏輯主要靠的是 HOC(高階組件)和 Render Props 這些組件設計模式,React 在原生層面並無提供相關的途徑。這些設計模式並不是萬能,它們在實現邏輯複用的同時,也破壞着組件的結構,其中一個最多見的問題就是「嵌套地獄」現象。

(2)函數組件

函數組件就是以函數的形態存在的 React 組件。函數組件內部沒法定義和維護 state,所以它還有一個別名叫「無狀態組件」。下面是一個函數組件:編程

import React from 'react';

function FunctionComponent(props) {
  const { text } = props
  return (
    <div> <p>{`函數組件接收的內容:${text}`}</p> </div>
  );
}

export default FunctionComponent
複製代碼

相比於類組件,函數組件肉眼可見的特質天然包括輕量、靈活、易於組織和維護、較低的學習成本等。實際上,類組件和函數組件之間,是面向對象函數式編程這兩個設計思想之間的差別。而函數組件更加契合 React 框架的設計理念: image.pngredux

React 組件自己的定位就是函數:輸入數據,輸出 UI 的函數。React 框架的主要工做就是及時地把聲明式的代碼轉換爲命令式的 DOM 操做,把數據層面的描述映射到用戶可見的 UI 變化中。從原則上來說,React 的數據應該老是牢牢地和渲染綁定在一塊兒的,而類組件沒法作到這一點。函數組件就真正地將數據和渲染綁定到一塊兒。函數組件是一個更加匹配其設計理念、也更有利於邏輯拆分與重用的組件表達形式。設計模式

爲了讓開發者更好的編寫函數組件。React Hooks 應運而生。數組

2. React Hooks是什麼?

(1)概念

爲了讓函數組件更有用,目標就是給函數組件加上狀態。咱們知道,函數和類不一樣,它並無一個實例的對象可以在屢次執行之間來保存狀態,那就須要一個函數外的空間來保存這個狀態,而且可以檢測狀態的變化,從而觸發組件的從新渲染。因此,咱們須要一個機制,將數據綁定到函數的執行。當數據變化時,函數能自動從新執行。這樣,任何會影響 UI 展示的外部數據,均可以經過這個機制綁定到 React 的函數組件上。而這個機制就是React Hooks。瀏覽器

實際上,React Hooks 是一套可以使函數組件更強大、更靈活的「鉤子」。在 React 中,Hooks 就是把某個目標結果鉤到某個可能會變化的數據源或者事件源上, 那麼當被鉤到的數據或事件發生變化時,產生這個目標結果的代碼會從新執行,產生更新後的結果。咱們知道,函數組件相對於類組件更適合去表達 React 組件的執行的,由於它更符合 State => View 邏輯關係,可是由於缺乏狀態、生命週期等機制,讓它一直功能受到限制,而 React Hooks 的出現,就是爲了幫助函數組件補齊這些缺失的能力。

下面就經過一個計數器,來看看使用類組件和React Hooks分別是如何實現的。

使用類組件實現:

import React from 'react'

class CounterClass extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0
    }
  }

  render() {
    return (
      <div> <h2>當前計數: {this.state.counter}</h2> <button onClick={e => this.increment()}>+1</button> <button onClick={e => this.decrement()}>-1</button> </div>
    )
  }

  increment() {
    this.setState({counter: this.state.counter + 1})
  }

  decrement() {
    this.setState({counter: this.state.counter - 1})
  }
}

export default CounterClass
複製代碼

使用React Hooks實現:

import React, { useState } from 'react';

function CounterHook() {
  const [counter, setCounter] = useState(0);

  return (
    <div> <h2>當前計數: {counter}</h2> <button onClick={e => setState(counter + 1)}>+1</button> <button onClick={e => setState(counter - 1)}>-1</button> </div>
  )
}

export default CounterHook
複製代碼

經過兩段代碼能夠看到,使用React Hooks實現的代碼更加簡潔,邏輯更加清晰。

(2)特色

React的特色主要有如下兩點:簡化邏輯複用有助於關注分離。

1)簡化邏輯複用

在出現Hooks 以前,組件邏輯的複用是比較難實現的,咱們必須藉助高階組件(HOC)和Render Props 這些組件設計模式來實現React Hooks出現以後,這些問題就迎刃而解了。

下面來舉一個例子:咱們有多個組件,當用戶調整瀏覽器的窗口大小是,須要從新調整頁面的佈局。在React中,咱們會根據Size大小來渲染不一樣的組件。代碼以下:

function render() {
  return size === small ? <SmallComponent /> : <LargeComponent />;
}
複製代碼

這段代碼看起來很簡單。可是若是咱們使用類組件去實現時,就須要使用到高階組件來解決,下面就用高階組件來實現一下。

首先要定義一個高階組件,負責監聽窗口的大小的變化,並將變化後的值做爲props傳給下一個組件:

const withWindowSize = Component => {
 	class WrappedComponent extends React.PureComponent {
 		constructor(props) {
 			super(props);
 			this.state = {
 					size: this.getSize()
 			};
 		}
 		componentDidMount() {
      // 監聽瀏覽器窗口大小
   		window.addEventListener("resize", this.handleResize);
   	}
 		componentWillUnmount() {
      // 移除監聽
 			window.removeEventListener("resize", this.handleResize);
 		}
    getSize() {
 			return window.innerWidth > 1000 ? "large""small";
    }
 		handleResize = ()=> {
 			const currentSize = this.getSize();
 			this.setState({
 				size: this.getSize()
 			});
 		}
		render() {
      return <Component size={this.state.size} />;
 		}
 	}
  return WrappedComponent;
};
複製代碼

這樣就能夠調用withWindowSize方法來產生一個新組件,新組件自帶size屬性,例如:

class MyComponent extends React.Component{
 	render() {
 	const { size } = this.props;
 		return size === small ? <SmallComponent /> : <LargeComponent />;
  }
}

export default withWindowSize(MyComponent); 
複製代碼

能夠看到,爲了傳遞外部狀態(size),咱們不得不給組件外面再套一層,這一層只是爲了封裝一段可重用的邏輯。這樣寫缺點是顯而易見的:

  • 代碼不直觀,難以理解,給維護帶來巨大挑戰;
  • 增長不少額外的組件節點,每個高階組件都會多一層包裝,給調試帶來困難。

而React Hooks的出現,就讓這種實現變得很簡單:

const getSize = () => {
  return window.innerWidth > 1000 ? "large" : "small";
}
const useWindowSize = () => {
  const [size, setSize] = useState(getSize());
  useEffect(() => {
    const handler = () => {
      setSize(getSize())
    };
    window.addEventListener('resize', handler);
    return () => {
      window.removeEventListener('resize', handler);
    };
   }, []);
   return size;
};

// 使用
const Demo = () => {
  const size = useWindowSize();
  return size === small ? <SmallComponent /> : <LargeComponent />;
};
複製代碼

能夠看到,窗口大小是外部的一個數據狀態,經過 Hooks 的方式對其進行封裝, 從而將其變成一個可綁定的數據源。這樣,當窗口大小變化時,使用這個 Hook 的組件就會從新渲染。並且代碼也更加簡潔和直觀,不會產生額外的組件節點。

2)有助於關注分離

Hooks的另一大好處就是有助於關注分離,在類組件中,咱們須要同一個業務邏輯分散在不一樣的生命週期,好比上面是例子,咱們在在 componentDidMount 中監聽窗口代銷,在 componentWillUnmount 中去解綁監聽事件。而在函數組件中,咱們能夠將全部邏輯寫在一塊兒。經過Hooks的方式,把業務邏輯清晰地隔離開,可以讓代碼更加容易理解和維護。

固然 React Hooks 也不是完美的,它的缺點以下:

  • Hooks 不能徹底地爲函數組件補齊類組件的能力,好比 getSnapshotBeforeUpdate、componentDidCatch 這些生命週期,目前都仍是強依賴類組件的。
  • 在類組件中有時一些方法有不少實例,若是用函數組件來解決相同的問題,業務邏輯的拆分和組織是一個很大的挑戰。耦合和內聚的邊界有時很難把握,函數組件給了咱們必定程度的自由,但也對開發者的水平提出了更高的要求。
  • Hooks 在使用層面有着嚴格的規則約束,對於 React 開發者來講,若是不能牢記並踐行 Hooks 的使用原則,若是對 Hooks 的關鍵原理沒有紮實的把握,很容易出現預料不到的問題。

(3)使用場景

React Hooks的使用場景以下:

  • Hook的出現基本能夠代替以前全部使用類組件的地方;
  • 若是是一箇舊的項目,不須要將全部的代碼重構爲Hooks,由於它徹底向下兼容,能夠漸進式的使用它;
  • Hook只能在函數組件中使用,不能在類組件或函數組件以外的地方使用。

注意: Hook指的是相似於useState、 useEffect這樣的函數,Hooks是對這類函數的統稱。

(4)使用規範

Hooks規範以下:

  • 始終在 React 函數的頂層使用 Hooks,遵循此規則,能夠確保每次渲染組件時都以相同的順序調用 Hook, 這就是讓 React 在多個useStateuseEffect 調用之間正確保留 Hook 的狀態的緣由;
  • Hooks 僅在 React 函數中使用。

Eslint Plugin 提供了 eslint-plugin-react-hooks 讓咱們遵循上述兩種規範。其使用方法以下:

  1. 安裝插件 eslint-plugin-react-hooks:
npm install eslint-plugin-react-hooks --save-dev
複製代碼
  1. 在 eslint 的 config 中配置 Hooks 規則:
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // 檢查 hooks 規則
    "react-hooks/exhaustive-deps": "warn"  // 檢查 effect 的依賴
  }
}
複製代碼

3. useState:維護狀態

(1)基本使用

useState 是容許咱們在 React 函數組件中添加 state 的一個 Hook,使用形式以下:

import React, { useState } from 'react';

function Example() {
  const [state, setState] = useState(0);
  const [age, setAge] = useState(18);
}

export default Example
複製代碼

這裏調用 useState 方法時,就定義一個 state 變量,它的初始值爲0,它與 class 裏面的 this.state 提供的功能是徹底相同的。

對於 useState 方法:

(1)參數:初始化值,它能夠是任意類型,比 如數字、對象、數組等。若是不設置爲undefined;

(2)返回值:數組,包含兩個元素(一般經過數組解構賦值來獲取這兩個元素);

  • 元素一:當前狀態的值(第一次調用爲初始化值),該值是隻讀的,只能經過第二個元素的方法來修改它;
  • 元素二:設置狀態值的函數;

實際上,Hook 就是 JavaScript 函數,這個函數能夠幫助咱們鉤入 React State 以及生命週期等特性。useState 和類組件中的 setState相似。二者的區別在於,類組件中的 state 只能有一個。通常是把一個對象做爲一個 state,而後再經過對象不一樣的屬性來表示不一樣的狀態。而函數組件中用 useState 則能夠很容易地建立多個 state,更加語義化。

(2)複雜變量

上面定義的狀態變量(值類型數據)都比較簡單,那若是是一個複雜的狀態變量(引用類型數據),該如何實現更新呢?下面來看一個例子:

import React, { useState } from 'react'

export default function ComplexHookState() {

  const [friends, setFrineds] = useState(["zhangsan", "lisi"]);
  
  function addFriend() {
    friends.push("wangwu");
    setFrineds(friends);
  }

  return (
    <div> <h2>好友列表:</h2> <ul> { friends.map((item, index) => { return <li key={index}>{item}</li> }) } </ul> // 正確的作法 <button onClick={e => setFrineds([...friends, "wangwu"])}>添加朋友</button> // 錯誤的作法 <button onClick={addFriend}>添加朋友</button> </div>
  )
}
複製代碼

這裏定義的狀態是一個數組,若是想修改這個數組,須要從新定義一個數組來進行修改,在原數組上的修改不會引發組件的從新渲染。由於,React組件的更新機制對state只進行淺對比,也就是更新某個複雜類型數據時只要它的引用地址沒變,就不會從新渲染組件。所以,當直接向原數組增長數據時,就不會引發組件的從新渲染。

對於這種狀況,常見的作法就是使用擴展運算符(...)來將數組元素從新賦值給一個新數組,或者對原數據進行深拷貝獲得一個新的數據。

(3)獨立性

當一個組件須要多個狀態時,咱們能夠在組件中屢次使用 useState

const [age, setAge] = useState(17)
const [fruit, setFruit] = useState('apple')
const [todos, setTodos] = useState({text: 'learn Hooks'})
複製代碼

在這裏,每一個 Hook 都是相互獨立的。那麼當出現多個狀態時,react是如何保證它的獨立性呢?上面調用了三次 useState,每次都是傳入一個值,react 是怎麼知道這個值對應的是哪一個狀態呢?

其實在初始化時會建立兩個數組 statesetters,而且會設置一個光標 cursor = 0 , 在每次運行 useState 函數時,會將參數放到 state 中,並根據運行順序來依次增長光標 cursor 的值,接着在 setters 中放入對應的 set 函數,經過光標 cursorset 函數和 state 關聯起來,最後,即是將保存的 stateset 函數以數組的形式返回出去。好比在運行 setCount(15) 時,就會直接運行 set 函數,set 函數有相應的 cursor 值,而後改變 state

(4)缺點

state雖然便於維護狀態,但也有缺點。一旦組件有本身狀態,當組件從新建立時,就有恢復狀態的過程,這會讓組件變得更復雜。

好比一個組件想在服務器獲取用戶列表並顯示,若是把讀取到的數據放到本地的 state 裏,那麼每一個用到這個組件的地方,就都須要從新獲取一遍。 而若是經過一些狀態管理框架(例如redux),去管理全部組件的 state ,那麼組件自己就能夠是無狀態的。無狀態組件能夠成爲更純粹的表現層,沒有太多的業務邏輯,從而更易於使用、測試和維護。

4. useEffect:執行反作用

(1)基本使用

函數式組件經過 useState 具有了操控 state 的能力,修改 state 須要在適當的場景進行:類組件在組件生命週期中進行 state 更新,函數式組件中須要用 useEffect 來模擬生命週期。目前 useEffect 至關於類組件中的 componentDidMount、componentDidUpdate、componentWillUnmount 三個生命週期的綜合。也就是說,useEffect 聲明的回調函數會在組件掛載、更新、卸載的時候執行。實際上,useEffect的做用就是執行反作用, 而反作用就是上面所說的這些和當前執行結果無關的代碼。 手動操做 DOM、訂閱事件、網絡請求等都屬於React更新DOM的反作用。

useEffect 的使用形式以下:

useEffect(callBack, [])
複製代碼

useEffect 接收兩個參數,分別是回調函數依賴數組。爲了不每次渲染都執行全部的 useEffect 回調,useEffect 提供了第二個參數,該參數一個數組。只有在渲染時數組中的值發生了變化,纔會執行該 useEffect 的回調。

(2)使用示例

下面來看一個例子:

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

function App() {
  const [count, setCount] = useState(0)
  
  useEffect(() => {
    console.log(count + '值發生了改變')
  }, [count])
  
  function changeTheCount () {
    setCount(count + 1)
  }
  
  return (
    <div> <div onClick={() => changeTheCount()}> <p>{count}</p> </div> </div>
  ) 
}
export default App
複製代碼

上面的代碼執行後,點擊 3 次數字,count 的值變爲了 3,而且在控制檯打印了 4 次輸出。第一次是初次 DOM 渲染完畢,後面 3 次是每次點擊後改變了 count 值,觸發了 DOM 從新渲染。因而可知,每次依賴數組中的元素髮生改變以後都會執行 effect 函數。

useEffect 還有兩個特殊的用法:沒有依賴項依賴項爲空數組。

1)沒有依賴項

對於下面的代碼,若是沒有依賴項,那它會在每次render以後執行:

useEffect(() => {
    console.log(count + '值發生了改變')
})
複製代碼

2)依賴項爲空數組

對於下面的代碼, 若是依賴項爲空數組,那它會在首次執行時觸發,對應到類組件的生命週期就是 componentDidMount。

useEffect(() => {
    console.log(count + '值發生了改變')
}, [])
複製代碼

除此以外,useEffect 還容許返回一個方法,用於在組件銷燬時作一些清理操做,以防⽌內存泄漏。好比移除事件的監聽。這個機制就至關於類組件生命週期中的 componentWillUnmount。好比清除定時器:

const [data, setData] = useState(new Date());
useEffect(() => {
 	const timer = setInterval(() => {
  	 setDate(new Date());
  }, 1000);
  return () => clearInterval(timer);
}, []);
複製代碼

經過這樣的機制,就可以更好地管理反作用,從而確保組件和反作用的一致性。

(3)總結

從上面的示例中能夠看到,useEffect主要有如下四種執行時機:

  • 每次 render 後執行:不提供第二個依賴項參數。好比:useEffect(() => {})
  • 組件 Mount 後執行:提供一個空數組做爲依賴項。好比:useEffect(() => {}, [])
  • 第一次以及依賴項發生變化後執行:提供依賴項數組。好比:useEffect(() => {}, [deps])
  • 組件 unmount 後執行:返回一個回調函數。好比:useEffect() => { return () => {} }, [])

在使用useEffect時,須要注意如下幾點:

  • 依賴數組中的依賴項必定是要在回調函數中使用的,否則就沒有任何意義;
  • 依賴項通常是一個常量數組,由於在建立回調函數時,就應該肯定依賴項了;
  • React在每次執行時使用的是淺比較,因此必定要注意對象和數組類型的依賴項。

5. useCallback:緩存回調函數

在類組件的 shouldComponentUpdate 中能夠經過判斷先後的 propsstate 的變化,來判斷是否須要阻止更新渲染。但使用函數組件形式失去了這一特性,沒法經過判斷先後狀態來決定是否更新,這就意味着函數組件的每一次調用都會執行其內部的全部邏輯,會帶來較大的性能損耗。useMemouseCallback 的出現就是爲了解決這一性能問題。

(1)使用場景

在React函數組件中,每次UI發生變化,都是經過從新執行這個函數來完成的,這和類組件有很大的差異:函數組件沒法在每次渲染之間維持一個狀態。

好比下面這個計數器的例子:

function Counter() {
 const [count, setCount] = useState(0);
 const increment = () => setCount(count + 1);
 return <button onClick={increment}>+</button>
}
複製代碼

因爲增長計數的方法increment在組件內部,這就致使在每次修改count時,都會從新渲染這個組件,increment也就沒法進行重用,每次都須要建立一個新的increment方法。

不只如此,即便count沒有發生改變,當組件內部的其餘state發生變化時,組件也會進行從新渲染,那這裏的increment方法也會所以從新建立。雖然這些都不影響頁面的正常使用,可是這增長了系統的開銷,而且每次建立新函數的方式會讓接收事件處理函數的組件從新渲染。

對於這種狀況,那上面的例子來講,咱們想要的就是:只有count發生變化時,對應的increment方法纔會從新建立。這裏就用到useCallback。

(2)基本使用

useCallback會返回一個函數的記憶的值,在依賴不變的狀況下,屢次定義時,返回的值是相同的。它的使用形式以下:

useCallback(callBack, [])
複製代碼

它的使用形式和useEffect相似,第一個參數是定義的回調函數,第二個參數是依賴的變量數組。只有當某個依賴變量發生變化時,纔會從新聲明定義的回調函數。

因爲useCallback在依賴項發生變化時返回的是函數,因此沒法很好的判斷返回的函數是否發生變動,這裏藉助ES6中的數據類型Set來判斷:

import React, { useState, useCallback } from "react";

const set = new Set();

export default function Callback() {
  const [count, setCount] = useState(1);
  const [value, setValue] = useState(1);

  const callback = useCallback(() => {
    console.log(count);
  }, [count]);
  set.add(callback);

  return (
    <div> <h1>Count: {count}</h1> <h1>Set.size: {set.size}</h1> <h1>Value: {value}</h1> <div> <button onClick={() => setCount(count + 1)}>Count + 1</button> <button onClick={() => setValue(value + 2)}>Value + 2</button> </div> </div>
  );
}

複製代碼

運行效果以下圖所示: wu7h5-46iw4.gif 能夠看到,當咱們點擊Count + 1按鈕時,Count和Set.size都增長1,說明產生了新的回調函數。當點擊Value + 2時,只有Value發生了變化,而Set.size沒有發生變化,說明沒有產生的新的回調函數,返回的是緩存的舊版本函數。

既然咱們知道了useCallback有這樣的特色,那在什麼狀況下能發揮出它的做用呢?

使用場景: 父組件中一個子組件,經過狀況下,當父組件發生更新時,它的子組件也會隨之更新,在多數狀況下,子組件隨着父組件更新而更新是沒有必要的。這時就能夠藉助useCallback來返回函數,而後把這個函數做爲props傳遞給子組件,這樣,子組件就能夠避免沒必要要的更新。

import React, { useState, useCallback, useEffect } from "react";
export default function Parent() {
  const [count, setCount] = useState(1);
  const [value, setValue] = useState(1);

  const callback = useCallback(() => {
    return count;
  }, [count]);

  return (
    <div> <h1>Parent: {count}</h1> <h1>Value: {value}</h1> <Child callback={callback} /> <div> <button onClick={() => setCount(count + 1)}>Count + 1</button> <button onClick={() => setValue(value + 2)}>Value + 2</button> </div> </div>
  );
}

function Child({ callback }) {
  const [count, setCount] = useState(() => callback());
  useEffect(() => {
    setCount(callback());
  }, [callback]);

  return <h2>Child: {count}</h2>;
}
複製代碼

對於這段代碼,運行結果以下:

dz8qw-hdm40.gif

能夠看到,當咱們點擊Counte + 1按鈕時,Parent和Child都會加一;當點擊Value + 1按鈕時,只有Value增大了,Child組件中的數據並無變化,因此就不會從新渲染。這樣就避免了一些無關的操做而形成子組件隨父組件而從新渲染。

除了上面的例子,全部依賴本地狀態或props來建立函數,須要使用到緩存函數的地方,都是useCallback的應用場景。一般使用useCallback的目的是不但願子組件進行屢次渲染,而不是爲了對函數進行緩存。

6. useMemo:緩存計算結果

useMemo實際的目的也是爲了進行性能的優化。

(1)使用場景

下面先來看一段代碼:

import React, { useState } from "react";

export default function WithoutMemo() {
  const [count, setCount] = useState(1);
  const [value, setValue] = useState(1);

  function expensive() {
    console.log("compute");
    let sum = 0;
    for (let i = 0; i < count * 100; i++) {
      sum += i;
    }
    return sum;
  }

  return (
    <div> <h1>Count: {count}</h1> <h1>Value: {value}</h1> <h1>Expensive: {expensive()}</h1> <div> <button onClick={() => setCount(count + 1)}>Count + 1</button> <button onClick={() => setValue(value + 2)}>Value + 2</button> </div> </div>
  );
}
複製代碼

這段代碼很簡單,expensive方法用來計算0到100倍count的和,這個計算是很昂貴的。當咱們點擊頁面的兩個按鈕時,expensive方法都是執行(能夠在控制檯看到),運行結果以下圖所示: 523le-wbz6x.gif

咱們知道,這個expensive方法只依賴於count,只有當count發生變化時才須要從新計算。在這種狀況下,咱們就能夠 useMemo,只在count的值修改時,纔去執行expensive的計算。

(2)使用示例

useMemo返回的也是一個記憶的值,在依賴不變的狀況下,屢次定義時,返回的值是相同的。它的使用形式以下:

useCallback(callBack, [])
複製代碼

它的使用形式和上面的useCallback相似,第一個參數是產生所需數據的計算函數,通常它會使用第二個參數中依賴數組的依賴項來生成一個結果,用來渲染最終的UI。

下面就使用useMemo來優化上面的代碼:

import React, { useState, useMemo } from "react";

export default function WithoutMemo() {
  const [count, setCount] = useState(1);
  const [value, setValue] = useState(1);

  const expensive = useMemo(() => {
    console.log("expensive執行");
    let sum = 0;
    for (let i = 0; i < count * 100; i++) {
      sum += i;
    }
    return sum;
  }, [count]);

  return (
    <div> <h1>Count: {count}</h1> <h1>Value: {value}</h1> <h1>Expensive: {expensive}</h1> <div> <button onClick={() => setCount(count + 1)}>Count + 1</button> <button onClick={() => setValue(value + 2)}>Value + 2</button> </div> </div>
  );
}

複製代碼

代碼的運行結果以下圖: 8av5y-l8o6c.gif

能夠看到,當點解Count + 1按鈕時,expensive方法纔會執行;而當點擊Value + 1按鈕時,expensive方法是不執行的。這裏咱們使用useMemo來執行昂貴的計算,而後將計算值返回,而且將count做爲依賴值傳遞進去。這樣只會在count改變時觸發expensive的執行,在修改value時,返回的是上一次緩存的值。

因此,當某個數據是經過其它數據計算獲得的,那麼只有當用到的數據,也就是依賴的數據發生變化的時候,才應該須要從新計算。useMemo能夠避免在用到的數據沒發生變化時進行重複的計算。

除此以外,useMemo 還有一個很重要的用處:避免子組件的重複渲染, 這和上面的useCallback是很相似的,這裏就不舉例說明了。

能夠看到,useMemo和useCallback是很相似的,它們之間是能夠相互轉化的:useCallback(fn, deps) 至關於 useMemo(() => fn, deps) 。

7. useRef:共享數據

函數組件雖然看起來很直觀,可是到目前爲止,它相對於類組件還缺乏一個很重要的能力,那就是組件屢次渲染之間共享數據。在類函數中,咱們能夠經過對象屬性來保存數據狀態。可是在函數組件中,沒有這樣一個空間去保存數據。所以,useRef 就提供了這樣的功能。

useRef的使用形式以下:

const myRefContainer = useRef(initialValue);
複製代碼

useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化爲傳入的參數。返回的 ref 對象在組件的整個生命週期內保持不變,也就是說每次從新渲染函數組件時,返回的 ref 對象都是同一個。

那在實際應用中,useRef有什麼用呢?主要有兩個應用場景:

(1)綁定DOM

有這樣一個簡單的場景:在初始化頁面時,使得頁面中的某個input輸入框自動聚焦,使用類組件能夠這樣實現:

class InputFocus extends React.Component {
  refInput = React.createRef();
  componentDidMount() {
    this.refInput.current && this.refInput.current.focus();
  }
  render() {
    return <input ref={this.refInput} />;
  }
}
複製代碼

那在函數組件中想要實現,能夠藉助useRef來實現:

function InputFocus() {
  const refInput = React.useRef(null);
  React.useEffect(() => {
    refInput.current && refInput.current.focus();
  }, []);

  return <input ref={refInput} />;
}
複製代碼

這裏,咱們將refInput和input輸入框綁定在了一塊兒,當咱們刷新頁面後,鼠標仍然是聚焦在這個輸入框的。

(2)保存數據

這樣一個場景,就是咱們有一個定時器組件,這個組件能夠開始和暫停,咱們可使用setInterval來進行計時,爲了能暫停,咱們就須要獲取到定時器的的引用,在暫停時清除定時器。那麼這個計時器引用就能夠保存在useRef中,由於它能夠存儲跨渲染的數據,代碼以下:

import React, { useState, useCallback, useRef } from "react";

export default function Timer() {
  const [time, setTime] = useState(0);
  const timer = useRef(null);

  const handleStart = useCallback(() => {
    timer.current = window.setInterval(() => {
      setTime((time) => time + 1);
    }, 100);
  }, []);

  const handlePause = useCallback(() => {
    window.clearInterval(timer.current);
    timer.current = null;
  }, []);

  return (
    <div> <p>{time / 10} seconds</p> <button onClick={handleStart}>開始</button> <button onClick={handlePause}>暫停</button> </div>
  );
}
複製代碼

能夠看到,這裏使用 useRef 建立了一個保存 setInterval 的引用,從而可以在點擊暫停時清除定時器,達到暫停的目的。同時,使用 useRef 保存的數據通常是和 UI 的渲染無關的,當 ref 的值發生變化時,不會觸發組件的從新渲染,這也是 useRef 區別於 useState 的地方。

8. useContext:全局狀態管理

咱們知道,React提供了Context來管理全局的狀態,當咱們在組件上建立一個 Context 時,這個組件樹上的全部組件就都都能訪問和修改這個 Context了。這個屬性適用於類組件。在React Hooks中也提供了相似的屬性,那就是useContext。

簡單來講就是 useContext 會建立一個上下文對象,而且對外暴露提供者和消費者,在上下文以內的全部子組件,均可以訪問這個上下文環境以內的數據。

context 作的事情就是建立一個上下文對象,而且對外暴露提供者和消費者,在上下文以內的全部子組件,均可以訪問這個上下文環境以內的數據,而且不用經過 props。 簡單來講, context 的做用就是對它所包含的組件樹提供全局共享數據的一種技術。

首先,建立一個上下文,來提供兩種不一樣的頁面主題樣式:

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};
const ThemeContext = React.createContext(themes.light)
複製代碼

接着,建立一個 Toolbar 組件,這個組件中包含了一個 ThemedButton 組件,這裏先不關心 ThemedButton 組件的邏輯:

function Toolbar(props) {
  return (
    <div> <ThemedButton /> </div>
  );
}
複製代碼

這時,須要提供者提供數據,提供者通常位於比較高的層級,直接放在 App 中。ThemeContext.Provider 就是這裏的提供者,接收的 value 就是它要提供的上下文對象:

function App() {
  return (
    <ThemeContext.Provider value={themes.light}> <Toolbar /> </ThemeContext.Provider>
  );
}
複製代碼

而後,消費者獲取數據,這是在 ThemedButton 組件中使用:

function ThemedButton(props) {
  const theme = useContext(ThemeContext);
  const [themes, setthemes] = useState(theme.dark);

  return (
    <div> <div style={{ width: "100px", height: "100px", background: themes.background, color: themes.foreground }} ></div> <button onClick={() => setthemes(theme.light)}>Light</button> <button onClick={() => setthemes(theme.dark)}>Dark</button> </div>
  );
}
複製代碼

到這裏,整個例子就結束了,下面是總體的代碼:

import React, { useContext, useState } from "react";

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

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

function ThemedButton(props) {
  const theme = useContext(ThemeContext);
  const [themes, setthemes] = useState(theme.dark);

  return (
    <div> <div style={{ width: "100px", height: "100px", background: themes.background, color: themes.foreground }} ></div> <button onClick={() => setthemes(theme.light)}>Light</button>&nbsp;&nbsp; <button onClick={() => setthemes(theme.dark)}>Dark</button> </div>
  );
}

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

export default function App() {
  return (
    <ThemeContext.Provider value={themes}> <Toolbar /> </ThemeContext.Provider>
  );
}
複製代碼

這裏經過使用useContext獲取到了頂層上下文中的themes數據,運行效果以下: 1c3ot-2hkhi.gif 這裏咱們的 useContext 看上去就是一個全局數據,那爲何要設計這樣一個複雜的機制,而不是直接用一個全局的變量去保存數據呢?其實就是爲了可以進行數據的綁定。當 useContext 的數據發生變化時,使用這個數據的組件就可以自動刷新。但若是沒有 useContext,而是使用一個簡單的全局變量,就很難去實現數據切換了。

實際上,Context就至關於提供了一個變量的機制,而全局變量就意味着:

  • 會讓調試變困難,由於很難跟蹤某個 Context 的變化到底是如何產生的。
  • 讓組件的複用變得困難,由於一個組件若是使用了某個 Context,它就必須確保被用到的地方必須有這個 Context 的 Provider 在其父組件的路徑上。

因此,useContext是一把雙刃劍,仍是要根據實際的業務場景去酌情使用。

9. useReducer:useState替代方案

在 Hooks 中提供了一個 API useReducer,它是 useState 的一種替代方案。

首先來看 useReducer 的語法:

const [state, dispatch] = useReducer((state, action) => {
    // 根據派發的 action 類型,返回一個 newState
}, initialArg, init)
複製代碼

useReducer 接收 reducer 函數做爲參數,reducer 接收兩個參數,一個是 state,另外一個是 action,而後返回一個狀態 statedispatchstate 是返回狀態中的值,而 dispatch 是一個能夠發佈事件來更新 state 的函數。

既然它是 useState 的替代方案,那下面就來看看和 useState 有什麼不一樣: 1)使用useState實現:

import React, { useState } from 'react'
function App() {
  const [count, setCount] = useState(0) 
    
  return (
    <div> <h1>you click {count} times</h1> <input type="button" onClick={()=> setCount(count + 1)} value="click me" /> </div>
  ) 
}
export default App
複製代碼

2)使用useReducer實現:

import React, { useReducer } from "react";

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    default:
      throw new Error();
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div> <h1>you click {state.count} times</h1> <input type="button" onClick={() => dispatch({ type: "increment" })} value="click me" /> </div>
  );
}
export default App;

複製代碼

useState 對比發現改寫後的代碼變長了,其執行過程以下:

  • 點擊 click me 按鈕時,會觸發 click 事件;
  • click 事件裏是個 dispatch 函數,dispatch 發佈事件告訴 reducer 我執行了 increment 動做;
  • reducer 會去查找 increment,返回一個新的 state 值。

下面是 useReducer 的整個執行過程:

其實 useReducer 執行過程就三步:

  • 第一步:事件發生;
  • 第二步:dispatch(action);
  • 第三步:reducer 根據 action.type 返回一個新的 state。

雖然使用useReducer時代碼變長,可是理解起來好像更簡單明瞭了,這是 useReducer 的優勢之一。useReducer 主要有如下優勢:

  • 更好的可讀性;
  • reducer 可讓咱們把作什麼和怎麼作分開,上面的 demo 中在點擊了 click me 按鈕時,咱們要作的就是發起加 1 操做,至於加 1 的操做要怎麼去實現就都放在 reducer 中維護。組件中只須要考慮怎麼作,使得咱們的代碼能夠像用戶行爲同樣更加清晰;
  • state 處理都集中到 reducer,對 state 的變化更有掌控力,同時也更容易複用 state 邏輯變化代碼,特別是對於 state 變化很複雜的場景。

當遇到如下場景時,能夠優先使用 useReducer

  • state 變化很複雜,常常一個操做須要修改不少 state
  • 深層子組件裏去修改一些狀態;
  • 應用程序比較大,UI 和業務須要分開維護。

最後: 到這裏就結束了,這篇文章只介紹了React Hooks的簡單使用。最近在深刻學習Hooks,期待下一篇文章!

相關文章
相關標籤/搜索