最近在專案中基本上全部使用了React Hooks,歷史專案也用React Hooks重寫了一遍,相比於Class元件,React Hooks的優點可以一句話來概括:就是簡單,在React hooks中沒有複雜的生命週期,沒有類元件中複雜的this指向,沒有類似於HOC,render props等複雜的元件複用模式等。本篇文章主要總結一下在React hooks工程實踐中的經驗。
- React hooks中的渲染行為
- React hooks中的效能優化
- React hooks中的狀態管理和通訊
原文首發至我的部落格: github.com/fortheallli…
一、React hooks中的渲染行為
1.React hooks元件是如何渲染的
理解React hooks的關鍵,就是要明白,hooks元件的每一次渲染都是獨立,每一次的render都是一個獨立的作用域,擁有自己的props和states、事件處理函式等。概括來講:
每一次的render都是一個互不相關的函式,擁有完全獨立的函式作用域,執行該渲染函式,返回相應的渲染結果
而類元件則不同,類元件中的props和states在整個生命週期中都是指向最新的那次渲染.
React hooks元件和類元件的在渲染行為中的區別,看起來很繞,我們可以用圖來區別,
上圖表示在React hooks元件的渲染過程,從圖中可以看出,react hooks元件的每一次渲染都是一個獨立的函式,會生成渲染區專屬的props和state. 接著來看類元件中的渲染行為:
類元件中在渲染開始的時候會在類元件的建構函式中生成一個props和state,所有的渲染過程都是在一個渲染函式中進行的並且,每一次的渲染中都不會去生成新的state和props,而是將值賦值給最開始被初始化的this.props和this.state。
2.工程中注意React hooks的渲染行為
理解了React hooks的渲染行為,就指示了我們如何在工程中使用。首先因為React hooks元件在每一次渲染的過程中都會生成獨立的所用域,因此,在元件內部的子函式和變數等在每次生命的時候都會重新生成,因此我們應該減少在React hooks元件內部宣告函式。
寫法一:
function App() {
const [counter, setCounter] = useState(0);
function formatCounter(counterVal) {
return `The counter value is ${counterVal}`;
}
return (
<div className="App">
<div>{formatCounter(counter)}</div>
<button onClick={() => setCounter(prevState => ++prevState)}>
Increment
</button>
</div>
);
}
複製程式碼
寫法二:
function formatCounter(counterVal) {
return `The counter value is ${counterVal}`;
}
function App() {
const [counter, setCounter] = useState(0);
return (
<div className="App">
<div>{formatCounter(counter)}</div>
<button onClick={()=>onClick(setCounter)}>
Increment
</button>
</div>
);
}
複製程式碼
App元件是一個hooks元件,我們知道了React hooks的渲染行為,那麼寫法1在每次render的時候都會去重新宣告函式formatCounter,因此是不可取的。我們推薦寫法二,如果函式與元件內的state和props無相關性,那麼可以宣告在元件的外部。如果函式與元件內的state和props強相關性,那麼我們下節會介紹useCallback和useMemo的方法。
React hooks中的state和props,在每次渲染的過程中都是重新生成和獨立的,那麼我們如果需要一個物件,從開始到一次次的render1 , render2, ...中都是不變的應該怎麼做呢。(這裡的不變是不會重新生成,是引用的地址不變的意思,其值可以改變)
我們可以使用useRef,建立一個“常量”,該常量在元件的渲染期內始終指向同一個引用地址。
通過useRef,可以實現很多功能,比如在某次渲染的時候,拿到前一次渲染中的state。
function App(){
const [count,setCount] = useState(0)
const prevCount = usePrevious(count);
return (
<div>
<h1>Now: {count}, before: {prevCount}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
複製程式碼
上述的例子中,我們通過useRef()建立的ref物件,在整個usePrevious元件的週期內都是同一個物件,我們可以通過更新ref.current的值,來在App元件的渲染過程中,記錄App元件渲染中前一次渲染的state.
這裡其實還有一個不容易理解的地方,我們來看usePrevious:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
複製程式碼
這裡的疑問是:為什麼當value改變的時候,返回的ref.current指向的是value改變之前的值?
也就是說:
為什麼useEffect在return ref.current之後才執行?
為了解釋這個問題,我們來聊聊神奇的useEffect.
3.神奇的useEffect
hooks元件的每一次渲染都可以看成一個個獨立的函式 render1,render2 ... rendern,那麼這些render函式之間是怎麼關聯的呢,還有上小節的問題,為什麼在usePrevious中,useEffect在return ref.current之後才執行。帶著這兩個疑問我們來看看在hooks元件中,最為神奇的useEffect。
用一句話概括就是:
每一渲染都會生成不同的render函式,並且每一次渲染通過useEffect會生成一個不同的Effects,Effects在每次渲染後聲效。
每次渲染除了生成不同的作用域外,如果該hooks元件中使用了useEffect,通過useEffect還會生成一個獨有的effects,該effects在渲染完成後生效。
舉例來說:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
複製程式碼
上述的例子中,完成的邏輯是:
- 渲染初始的內容:
<p>You clicked 0 times</p>
- 渲染完成之後呼叫這個effect:{ document.title = 'You clicked 0 times' }。
- 點選Click me
- 渲染新的內容渲染的內容:
<p>You clicked 1 times</p>
- 渲染完成之後呼叫這個effect:() => { document.title = 'You clicked 1 times' }。
也就是說每次渲染render中,effect位於同步執行佇列的最後面,在dom更新或者函式返回後在執行。
我們在來看usePrevious的例子:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
複製程式碼
因為useEffect的機制,在新的渲染過程中,先返回ref.current再執行deps依賴更新ref.current,因此usePrevios總是返回上一次的值。
現在我們知道,在一次渲染render中,有自己獨立的state,props,還有獨立的函式作用域,函式定義,effects等,實際上,在每次render渲染中,幾乎所有都是獨立的。我們最後來看兩個例子:
(1)
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
複製程式碼
(2)
function Counter() {
const [count, setCount] = useState(0);
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
複製程式碼
這兩個例子中,我們在3內點選5次Click me按鈕,那麼輸出的結果都是一樣的。
You clicked 0 times You clicked 1 times You clicked 2 times You clicked 3 times You clicked 4 times You clicked 5 times
總而言之,每一次渲染的render,幾乎都是獨立和獨有的,除了useRef建立的物件外,其他物件和函式都沒有相關性.
二、React hooks中的效能優化
前面我們講了React hooks中的渲染行為,也初步 提到了說將與state和props無關的函式,宣告在hooks元件外面可以提高元件的效能,減少每次在渲染中重新宣告該無關函式. 除此之外,React hooks還提供了useMemo和useCallback來優化元件的效能.
(1).useCallback
有些時候我們必須要在hooks元件內定義函式或者方法,那麼推薦用useCallback快取這個方法,當useCallback的依賴項不發生變化的時候,該函式在每次渲染的過程中不需要重新宣告
useCallback接受兩個引數,第一個引數是要快取的函式,第二個引數是一個陣列,表示依賴項,當依賴項改變的時候會去重新宣告一個新的函式,否則就返回這個被快取的函式.
function formatCounter(counterVal) {
return `The counter value is ${counterVal}`;
}
function App(props) {
const [counter, setCounter] = useState(0);
const onClick = useCallback(()=>{
setCounter(props.count)
},[props.count]);
return (
<div className="App">
<div>{formatCounter(counter)}</div>
<button onClick={onClick}>
Increment
</button>
</div>
);
}
複製程式碼
上述例子我們在第一章的例子基礎上增加了onClick方法,並快取了這個方法,只有props中的count改變的時候才需要重新生成這個方法。
(2).useMemo
useMemo與useCallback大同小異,區別就是useMemo快取的不是函式,快取的是物件(可以是jsx虛擬dom物件),同樣的當依賴項不變的時候就返回這個被快取的物件,否則就重新生成一個新的物件。
為了實現元件的效能優化,我們推薦:
在react hooks元件中宣告的任何方法,或者任何物件都必須要包裹在useCallback或者useMemo中。
(3)useCallback,useMemo依賴項的比較方法
我們來看看useCallback,useMemo的依賴項,在更新前後是怎麼比較的
import is from 'shared/objectIs';
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
return false;
}
if (nextDeps.length !== prevDeps.length) {
return false
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
複製程式碼
其中is方法的定義為:
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y)
);
}
export default (typeof Object.is === 'function' ? Object.is : is);
複製程式碼
這個is方法就是es6的Object.is的相容性寫法,也就是說在useCallback和useMemo中的依賴項前後是通過Object.is來比較是否相同的,因此是淺比較。
三、React hooks中的狀態管理和通訊
react hooks中的區域性狀態管理相比於類元件而言更加簡介,那麼如果我們元件採用react hooks,那麼如何解決元件間的通訊問題。
(1) UseContext
最基礎的想法可能就是通過useContext來解決元件間的通訊問題。
比如:
function useCounter() {
let [count, setCount] = useState(0)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return { count, decrement, increment }
}
let Counter = createContext(null)
function CounterDisplay() {
let counter = useContext(Counter)
return (
<div>
<button onClick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onClick={counter.increment}>+</button>
</div>
)
}
function App() {
let counter = useCounter()
return (
<Counter.Provider value={counter}>
<CounterDisplay />
<CounterDisplay />
</Counter.Provider>
)
}
複製程式碼
在這個例子中通過createContext和useContext,可以在App的子元件CounterDisplay中使用context,從而實現一定意義上的元件通訊。
此外,在useContext的基礎上,為了其整體性,業界也有幾個比較簡單的封裝:
github.com/jamiebuilds… github.com/diegohaz/co…
但是其本質都沒有解決一個問題:
如果context太多,那麼如何維護這些context
也就是說在大量元件通訊的場景下,用context進行元件通訊程式碼的可讀性很差。這個類元件的場景一致,context不是一個新的東西,雖然用了useContext減少了context的使用複雜度。
(2) Redux結合hooks來實現元件間的通訊
hooks元件間的通訊,同樣可以使用redux來實現。也就是說:
在React hooks中,redux也有其存在的意義
在hooks中存在一個問題,因為不存在類似於react-redux中connect這個高階元件,來傳遞mapState和mapDispatch, 解決的方式是通過redux-react-hook或者react-redux的7.1 hooks版本來使用。
- redux-react-hook
在redux-react-hook中提供了StoreContext、useDispatch和useMappedState來操作redux中的store,比如定義mapState和mapDispatch的方式為:
import {StoreContext} from 'redux-react-hook';
ReactDOM.render(
<StoreContext.Provider value={store}>
<App />
</StoreContext.Provider>,
document.getElementById('root'),
);
import {useDispatch, useMappedState} from 'redux-react-hook';
export function DeleteButton({index}) {
// Declare your memoized mapState function
const mapState = useCallback(
state => ({
canDelete: state.todos[index].canDelete,
name: state.todos[index].name,
}),
[index],
);
// Get data from and subscribe to the store
const {canDelete, name} = useMappedState(mapState);
// Create actions
const dispatch = useDispatch();
const deleteTodo = useCallback(
() =>
dispatch({
type: 'delete todo',
index,
}),
[index],
);
return (
<button disabled={!canDelete} onClick={deleteTodo}>
Delete {name}
</button>
);
}
複製程式碼
- react-redux 7.1的hooks版
這也是官方較為推薦的,react-redux 的hooks版本提供了useSelector()、useDispatch()、useStore()這3個主要方法,分別對應與mapState、mapDispatch以及直接拿到redux中store的例項.
簡單介紹一下useSelector,在useSelector中除了能從store中拿到state以外,還支援深度比較的功能,如果相應的state前後沒有改變,就不會去重新的計算.
舉例來說,最基礎的用法:
import React from 'react'
import { useSelector } from 'react-redux'
export const TodoListItem = props => {
const todo = useSelector(state => state.todos[props.id])
return <div>{todo.text}</div>
}
複製程式碼
實現快取功能的用法:
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const selectNumOfDoneTodos = createSelector(
state => state.todos,
todos => todos.filter(todo => todo.isDone).length
)
export const DoneTodosCounter = () => {
const NumOfDoneTodos = useSelector(selectNumOfDoneTodos)
return <div>{NumOfDoneTodos}</div>
}
export const App = () => {
return (
<>
<span>Number of done todos:</span>
<DoneTodosCounter />
</>
)
}
複製程式碼
在上述的快取用法中,只要todos.filter(todo => todo.isDone).length不改變,就不會去重新計算.