「這是我參與8月更文挑戰的第6天,活動詳情查看: 8月更文挑戰」javascript
Hook 是 React 16.8 的新增特性,它可讓咱們在不編寫class的狀況下使用state以及其餘的React特性(好比生命週期)。React Hooks 的出現是對類組件和函數組件這兩種組件形式的思考和側重。下面就來看看函數組件和類組件分別有哪些優缺點。java
類組件是基於 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
對於類組件,總結其缺點以下:npm
函數組件就是以函數的形態存在的 React 組件。函數組件內部沒法定義和維護 state,所以它還有一個別名叫「無狀態組件」。下面是一個函數組件:編程
import React from 'react';
function FunctionComponent(props) {
const { text } = props
return (
<div> <p>{`函數組件接收的內容:${text}`}</p> </div>
);
}
export default FunctionComponent
複製代碼
相比於類組件,函數組件肉眼可見的特質天然包括輕量、靈活、易於組織和維護、較低的學習成本等。實際上,類組件和函數組件之間,是面向對象和函數式編程這兩個設計思想之間的差別。而函數組件更加契合 React 框架的設計理念: redux
React 組件自己的定位就是函數:輸入數據,輸出 UI 的函數。React 框架的主要工做就是及時地把聲明式的代碼轉換爲命令式的 DOM 操做,把數據層面的描述映射到用戶可見的 UI 變化中。從原則上來說,React 的數據應該老是牢牢地和渲染綁定在一塊兒的,而類組件沒法作到這一點。函數組件就真正地將數據和渲染綁定到一塊兒。函數組件是一個更加匹配其設計理念、也更有利於邏輯拆分與重用的組件表達形式。設計模式
爲了讓開發者更好的編寫函數組件。React Hooks 應運而生。數組
爲了讓函數組件更有用,目標就是給函數組件加上狀態。咱們知道,函數和類不一樣,它並無一個實例的對象可以在屢次執行之間來保存狀態,那就須要一個函數外的空間來保存這個狀態,而且可以檢測狀態的變化,從而觸發組件的從新渲染。因此,咱們須要一個機制,將數據綁定到函數的執行。當數據變化時,函數能自動從新執行。這樣,任何會影響 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實現的代碼更加簡潔,邏輯更加清晰。
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 也不是完美的,它的缺點以下:
React Hooks的使用場景以下:
注意: Hook指的是相似於useState、 useEffect這樣的函數,Hooks是對這類函數的統稱。
Hooks規範以下:
useState
和 useEffect
調用之間正確保留 Hook 的狀態的緣由;Eslint Plugin 提供了 eslint-plugin-react-hooks
讓咱們遵循上述兩種規範。其使用方法以下:
npm install eslint-plugin-react-hooks --save-dev
複製代碼
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 檢查 hooks 規則
"react-hooks/exhaustive-deps": "warn" // 檢查 effect 的依賴
}
}
複製代碼
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,更加語義化。
上面定義的狀態變量(值類型數據)都比較簡單,那若是是一個複雜的狀態變量(引用類型數據),該如何實現更新呢?下面來看一個例子:
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只進行淺對比,也就是更新某個複雜類型數據時只要它的引用地址沒變,就不會從新渲染組件。所以,當直接向原數組增長數據時,就不會引發組件的從新渲染。
對於這種狀況,常見的作法就是使用擴展運算符(...)來將數組元素從新賦值給一個新數組,或者對原數據進行深拷貝獲得一個新的數據。
當一個組件須要多個狀態時,咱們能夠在組件中屢次使用 useState
:
const [age, setAge] = useState(17)
const [fruit, setFruit] = useState('apple')
const [todos, setTodos] = useState({text: 'learn Hooks'})
複製代碼
在這裏,每一個 Hook 都是相互獨立的。那麼當出現多個狀態時,react是如何保證它的獨立性呢?上面調用了三次 useState
,每次都是傳入一個值,react 是怎麼知道這個值對應的是哪一個狀態呢?
其實在初始化時會建立兩個數組 state
和 setters
,而且會設置一個光標 cursor = 0
, 在每次運行 useState
函數時,會將參數放到 state
中,並根據運行順序來依次增長光標 cursor
的值,接着在 setters
中放入對應的 set
函數,經過光標 cursor
把 set
函數和 state
關聯起來,最後,即是將保存的 state
和 set
函數以數組的形式返回出去。好比在運行 setCount(15)
時,就會直接運行 set
函數,set
函數有相應的 cursor
值,而後改變 state
。
state雖然便於維護狀態,但也有缺點。一旦組件有本身狀態,當組件從新建立時,就有恢復狀態的過程,這會讓組件變得更復雜。
好比一個組件想在服務器獲取用戶列表並顯示,若是把讀取到的數據放到本地的 state 裏,那麼每一個用到這個組件的地方,就都須要從新獲取一遍。 而若是經過一些狀態管理框架(例如redux),去管理全部組件的 state ,那麼組件自己就能夠是無狀態的。無狀態組件能夠成爲更純粹的表現層,沒有太多的業務邏輯,從而更易於使用、測試和維護。
函數式組件經過 useState 具有了操控 state 的能力,修改 state 須要在適當的場景進行:類組件在組件生命週期中進行 state 更新,函數式組件中須要用 useEffect 來模擬生命週期。目前 useEffect 至關於類組件中的 componentDidMount、componentDidUpdate、componentWillUnmount 三個生命週期的綜合。也就是說,useEffect 聲明的回調函數會在組件掛載、更新、卸載的時候執行。實際上,useEffect的做用就是執行反作用, 而反作用就是上面所說的這些和當前執行結果無關的代碼。 手動操做 DOM、訂閱事件、網絡請求等都屬於React更新DOM的反作用。
useEffect 的使用形式以下:
useEffect(callBack, [])
複製代碼
useEffect 接收兩個參數,分別是回調函數與依賴數組。爲了不每次渲染都執行全部的 useEffect 回調,useEffect 提供了第二個參數,該參數一個數組。只有在渲染時數組中的值發生了變化,纔會執行該 useEffect 的回調。
下面來看一個例子:
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);
}, []);
複製代碼
經過這樣的機制,就可以更好地管理反作用,從而確保組件和反作用的一致性。
從上面的示例中能夠看到,useEffect主要有如下四種執行時機:
useEffect(() => {})
;useEffect(() => {}, [])
;useEffect(() => {}, [deps])
;useEffect() => { return () => {} }, [])
。在使用useEffect時,須要注意如下幾點:
在類組件的 shouldComponentUpdate
中能夠經過判斷先後的 props
和 state
的變化,來判斷是否須要阻止更新渲染。但使用函數組件形式失去了這一特性,沒法經過判斷先後狀態來決定是否更新,這就意味着函數組件的每一次調用都會執行其內部的全部邏輯,會帶來較大的性能損耗。useMemo
和 useCallback
的出現就是爲了解決這一性能問題。
在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。
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>
);
}
複製代碼
運行效果以下圖所示: 能夠看到,當咱們點擊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>;
}
複製代碼
對於這段代碼,運行結果以下:
能夠看到,當咱們點擊Counte + 1按鈕時,Parent和Child都會加一;當點擊Value + 1按鈕時,只有Value增大了,Child組件中的數據並無變化,因此就不會從新渲染。這樣就避免了一些無關的操做而形成子組件隨父組件而從新渲染。
除了上面的例子,全部依賴本地狀態或props來建立函數,須要使用到緩存函數的地方,都是useCallback的應用場景。一般使用useCallback的目的是不但願子組件進行屢次渲染,而不是爲了對函數進行緩存。
useMemo實際的目的也是爲了進行性能的優化。
下面先來看一段代碼:
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方法都是執行(能夠在控制檯看到),運行結果以下圖所示:
咱們知道,這個expensive方法只依賴於count,只有當count發生變化時才須要從新計算。在這種狀況下,咱們就能夠 useMemo,只在count的值修改時,纔去執行expensive的計算。
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>
);
}
複製代碼
代碼的運行結果以下圖:
能夠看到,當點解Count + 1按鈕時,expensive方法纔會執行;而當點擊Value + 1按鈕時,expensive方法是不執行的。這裏咱們使用useMemo來執行昂貴的計算,而後將計算值返回,而且將count做爲依賴值傳遞進去。這樣只會在count改變時觸發expensive的執行,在修改value時,返回的是上一次緩存的值。
因此,當某個數據是經過其它數據計算獲得的,那麼只有當用到的數據,也就是依賴的數據發生變化的時候,才應該須要從新計算。useMemo能夠避免在用到的數據沒發生變化時進行重複的計算。
除此以外,useMemo 還有一個很重要的用處:避免子組件的重複渲染, 這和上面的useCallback是很相似的,這裏就不舉例說明了。
能夠看到,useMemo和useCallback是很相似的,它們之間是能夠相互轉化的:useCallback(fn, deps) 至關於 useMemo(() => fn, deps) 。
函數組件雖然看起來很直觀,可是到目前爲止,它相對於類組件還缺乏一個很重要的能力,那就是組件屢次渲染之間共享數據。在類函數中,咱們能夠經過對象屬性來保存數據狀態。可是在函數組件中,沒有這樣一個空間去保存數據。所以,useRef 就提供了這樣的功能。
useRef的使用形式以下:
const myRefContainer = useRef(initialValue);
複製代碼
useRef
返回一個可變的 ref 對象,其 .current
屬性被初始化爲傳入的參數。返回的 ref 對象在組件的整個生命週期內保持不變,也就是說每次從新渲染函數組件時,返回的 ref 對象都是同一個。
那在實際應用中,useRef有什麼用呢?主要有兩個應用場景:
有這樣一個簡單的場景:在初始化頁面時,使得頁面中的某個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輸入框綁定在了一塊兒,當咱們刷新頁面後,鼠標仍然是聚焦在這個輸入框的。
這樣一個場景,就是咱們有一個定時器組件,這個組件能夠開始和暫停,咱們可使用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 的地方。
咱們知道,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> <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數據,運行效果以下: 這裏咱們的 useContext 看上去就是一個全局數據,那爲何要設計這樣一個複雜的機制,而不是直接用一個全局的變量去保存數據呢?其實就是爲了可以進行數據的綁定。當 useContext 的數據發生變化時,使用這個數據的組件就可以自動刷新。但若是沒有 useContext,而是使用一個簡單的全局變量,就很難去實現數據切換了。
實際上,Context就至關於提供了一個變量的機制,而全局變量就意味着:
因此,useContext是一把雙刃劍,仍是要根據實際的業務場景去酌情使用。
在 Hooks 中提供了一個 API useReducer
,它是 useState
的一種替代方案。
首先來看 useReducer
的語法:
const [state, dispatch] = useReducer((state, action) => {
// 根據派發的 action 類型,返回一個 newState
}, initialArg, init)
複製代碼
useReducer
接收 reducer
函數做爲參數,reducer
接收兩個參數,一個是 state
,另外一個是 action
,而後返回一個狀態 state
和 dispatch
,state
是返回狀態中的值,而 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
執行過程就三步:
雖然使用useReducer時代碼變長,可是理解起來好像更簡單明瞭了,這是 useReducer
的優勢之一。useReducer
主要有如下優勢:
reducer
可讓咱們把作什麼和怎麼作分開,上面的 demo 中在點擊了 click me
按鈕時,咱們要作的就是發起加 1 操做,至於加 1 的操做要怎麼去實現就都放在 reducer 中維護。組件中只須要考慮怎麼作,使得咱們的代碼能夠像用戶行爲同樣更加清晰;state
處理都集中到 reducer
,對 state
的變化更有掌控力,同時也更容易複用 state
邏輯變化代碼,特別是對於 state
變化很複雜的場景。當遇到如下場景時,能夠優先使用 useReducer
:
state
變化很複雜,常常一個操做須要修改不少 state
;最後: 到這裏就結束了,這篇文章只介紹了React Hooks的簡單使用。最近在深刻學習Hooks,期待下一篇文章!