React Hooks
Hook是React v16.8的新特性,可以用函數的形式代替原來的繼承類的形式,可以在不編寫
class
的情況下使用state
以及其他React特性
React 設計原理
- React認為,UI視圖是數據的一種視覺映射,
UI = F(Data)
,這里的F
主要負責對輸入數據進行加工,對數據變更做出相應 - 公式里的
F
在React里抽象成組件,React是以組件為粒度編排應用的,組件是代碼復用的最小單元 - 在設計上,React采用
props
屬性來接收外部數據,使用state
屬性來管理組件自身產生的數據,而為了實現(運行時)對數據變更做出相應需要,React采用基于類的組件設計 - 除此之外,React認為組件是有生命周期的,因此提供了一系列API供開發者使用
我們所熟悉的React組件長這樣
import React, { Component } from "react";
// React基于Class設計組件
export default class Button extends Component {
constructor() {
super();
// 組件自身數據
this.state = { buttonText: "Click me, please" };
this.handleClick = this.handleClick.bind(this);
}
// 響應數據變更
handleClick() {
this.setState({ buttonText: "Thanks, been clicked!" });
}
// 編排數據呈現UI
render() {
const { buttonText } = this.state;
return <button onClick={this.handleClick}>{buttonText}</button>;
}
}
組件類的缺點
上面實例代碼只是一個按鈕組件,但是可以看到,它的代碼已經很重了。真實的React App由多個類按照層級,一層層構成,復雜度成倍增長。再加入 Redux + React Router,就變得更復雜
很可能隨便一個組件最后export
出去就是醬紫的:
export default withStyle(style)(connect(/*something*/)(withRouter(MyComponent)))
一個4層嵌套HOC,嵌套地獄
同時,如果你的組件內事件多,那么你的constructor
就是醬紫的
class MyComponent extends React.Component {
constructor() {
// initiallize
this.handler1 = this.handler1.bind(this)
this.handler2 = this.handler2.bind(this)
this.handler3 = this.handler3.bind(this)
this.handler4 = this.handler4.bind(this)
this.handler5 = this.handler5.bind(this)
// ...more
}
}
而Function Component編譯后就是一個普通的function,function對js引擎是友好的,而Class Component在React內部是當做Javascript Function類來處理的,代碼很難被壓縮,比如方法名稱
還有this
啦,稍微不注意就會出現因this
指向報錯的問題等。。。
總結一下就是:
- 很難復用邏輯,會導致組件樹層級很深
- 會產生巨大的組件(很多代碼必須寫在類里面)
- 類組件很難理解,比如方法需要
bind
,this
的指向不明確 - 編譯size,性能問題
Hooks
State Hook
Hook是什么?
可以先通過一個例子來看看,在class中,我們通過在構造函數中設置this.state
初始化組件的state:
this.state = {
n: 0
}
而在函數組件中,我們沒有this
,所以我們不能分配或讀取this.state
,但是可以在組件中調用useState
Hook
import React, {useState} from 'react';
function xxx() {
const [n, setN] = useState(0);
}
在上面代碼中,useState
就是Hook
Hook是一個特殊的函數,它可以讓你“鉤入”React的特性。例如
useState
是允許你在React函數組件中添加state
的Hook。
如果你在編寫函數組件并意識到需要向其添加一些state
,以前的做法是必須將其轉化為Class。現在你可以在現有的函數組件中使用Hook
讓函數組件自身具備狀態處理能力,且自身能夠通過某種機制觸發狀態的變更并引起re-render,這種機制就是Hooks
走進useState
示例代碼:
import React, { useState } from 'react';
function App() {
// 聲明一個叫 "n" 的 state 變量
// useState接收一個參數作為初始值
// useState返回一個數組,[state, setState]
const [n, setN] = useState(0);
return (
<div>
{/* 讀取n,等同于this.state.n */}
<p>{n}</p>
{/* 通過setN更新n,等同于this.setN(n: this.state.n + 1) */}
<button onClick={() => setN(n + 1)}>
+1
</button>
</div>
);
}
運行一下(代碼1)
- 首次渲染 render
<App />
- 調用
App
函數,得到虛擬DOM對象,創建真實DOM - 點擊buttno調用
setN(n + 1)
,因為要更新頁面的n
,所以再次render<App />
- 重復第二步,從控制臺打印看出每次執行
setN
都會觸發App
函數運行,得到一個新的虛擬DOM,DOM Diff更新真實DOM
那么問題來了,首次運行App
函數和setN
時都調用了App
,兩次運行useState
是一樣的嗎?setN
改變n
的值了嗎?為什么得到了不一樣的n
,useState
的時候做了什么?
分析:
- setN
- setN一定會修改數據x,將n+1存入x
- setN一定會觸發
<App />
重新渲染(re-render)
- useState
- useState肯定會從x讀取n的最新值
- x
- 每個組件都有自己的數據x,我們將其命名為state
嘗試實現React.useState(代碼2)
// 和useState一樣,myUseState接收一個初始值,返回state和setState方法
const myUseState = initialValue => {
let state = initialValue
const setState = newValue => {
state = newValue
// 重新渲染
render()
}
return [state, setState]
}
const render = () => {
// 雞賊暴力渲染法
ReactDOM.render(<App />, rootElement)
}
function App() {
const [n, setN] = myUseState(0)
...
}
點擊button,n沒有任何變化
原來每次state
都變成了初始值0
,因為myUseState
會將state
重置
我們需要一個不會被myUseState
重置的變量,那么這個變量只要聲明在myUseState
外面即可
let _state;
const myUseState = initialValue => {
// 如果state是undefined,則賦給初始值,否則就賦值為保存在外面的_state
_state = _state === undefined ? initialValue : _state;
const setState = newValue => {
_state = newValue;
render();
};
return [_state, setState];
};
還有問題,如果一個組件有倆state咋整?由于所有數據都放在_state,產生沖突:
function App() {
const [n, setN] = myUseState(0)
const [m, setM] = myUseState(0)
...
}
解決:
- 把_state做成對象
- 不可行,沒有key,
useState(0)
只傳入了一個參數0
,并不知道是n
還是m
- 不可行,沒有key,
- 把_state做成數組
- 可行,
_state = [0, 0]
- 可行,
let _state = [];
let index = 0;
const myUseState = (initialValue) => {
const currentIndex = index;
_state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex];
const setState = (newValue) => {
_state[currentIndex] = newValue;
render();
};
index += 1;
return [_state[currentIndex], setState];
};
const render = () => {
// 重新渲染要重置index
index = 0;
ReactDOM.render(<App />, rootElement);
};
解決了存在多個state的情況,但是還有問題,就是useState
調用順序必須一致!
- 如果第一次渲染時n是第一個,m是第二個,k是第三個
- 則第二次渲染時必須保證順序一致,因為數組根據調用順序存儲值
- re-render時會從第一行代碼開始重新執行整個組件
- 所以React不允許出現如下代碼
React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.
最后一個問題:
App用了_state和index,那其他組件用什么?放在全局作用域重名怎么解決?
運行App后,React會維護一個虛擬DOM樹,每個節點都有一個虛擬DOM對象(Fiber),將_state,index存儲在對象上
額外擴展一下Fiber對象,它的數據結構如下:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance 實例
this.tag = tag;
this.key = key;
// JSX翻譯過來之后是React.createElement,他最終返回的是一個ReactElement對象
// 就是ReactElement的`?typeof`
this.elementType = null;
// 就是ReactElement的type,他的值就是<MyClassComponent />這個class,不是實例,實例是在render過程中創建
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
// 用來存儲state
// 記錄useState應該返回的結果
this.memoizedState = null;
this.firstContextDependency = null;
// ...others
}
總結:
- 每個函數組件對應一個React節點(FiberNode)
- 每個節點保存著
_state
(memorizedState)和index
(實際是鏈表) -
useState
會讀取對應節點的state[index] -
index
是由useState
的調用順序決定 -
setState
會修改_state
,并觸發更新
搞清楚useState
干了啥以后,回過頭再看setN
改變n
了嗎,為什么得到了不一樣的n
(代碼3)
- 先+1,后log => 1
- 先log,后+1 => 0
- 為什么log出了舊數據
分析:
- 先點擊log,
log(0)
三秒后執行,此時n
是0
,n
不會變 - 再點擊+1,此時調用的是一個新的函數,生成了新的
n
,re-render -
n=0
和n=1
同時存在內存中
結論:因為有多個n
,setN
并不會改變n
,React函數式編程決定了n的值不會被改變,只會被回收
注意事項:
- 不可局部更新(代碼4)
- 地址要變:
setState(obj)
如果obj
地址不變,那么React就認為數據沒有變化 - useState接受函數:函數返回初始state,且只執行一次
- setState接收函數:setN(i => i + 1),優先使用這種形式
useReducer
React本身不提供狀態管理功能,通常需要使用外部庫,最常用的庫是
Redux
Redux的核心概念是,將需要修改的state都存入到store里,發起一個action用來描述發生了什么,用reducers描述action如何改變state,真正能改變store中數據的是store.dispatch API
Reducer是一個純函數,只承擔計算 State 的功能,函數的形式是(state, action) => newState
Action是消息的載體,只能被別人操作,自己不能進行任何操作
useReducer()
鉤子用來引入Reducer功能(代碼5)
const [state, dispatch] = useReducer(reducer, initial)
上面是useReducer
基本用法
- 接受Reducer函數和一個初始值作為參數
- 返回一個數組,數組
[0]
位是狀態當前值,第[1]
位是dispatch
函數,用來發送action
似曾相識的感覺
const [n, setN] = useState(0)
// n:讀
// setN:寫
總的來說useReducer就是復雜版本的useState,那么什么時候使用useReducer,什么時候又使用useState呢?
看一個代碼6
當你需要維護多個state,那么為什么不用一個對象來維護呢,對象是可以合并的
需要注意的是,由于Hooks可以提供狀態管理和Reducer函數,所以在這方面可以取代Redux。但是,它沒法兒提供中間件(midddleware)和時間旅行(time travel),如果你需要這兩個功能,還是要用Redux。
中間件原理:封裝改造store.dispatch,將其指向中間件,以實現在dispatch和reducer之間處理action數據的邏輯,也可以將中間件看成是dispatch方法的封裝器
有沒有代替Redux的方法呢?
Reducer + Context
useContext
什么是上下文?
- 全局變量是全局的上下文
- 上下文是局部的全局變量
使用方法:
// 創建上下文
const c = createContext(null)
function App() {
const [n, setN] = useState(0)
return (
// 使用<c.Provider>圈定作用域
<c.Provider value={n, setN}>
<Father />
</ c.Provider>
)
}
function Father() {
return (
<div>我是爸爸
<Son />
</div>
)
}
function Son() {
// 在作用域中使用useContext(c)來獲取并使用上下文
// 要注意這里useContext返回的是對象,不是數組
const {n, setN} = useContext(c)
const onClick = () => {
setN( i => i + 1)
}
return (
<div>我是兒子,我可以拿到n:{n}
<button onClick={onClick}>我也可以更新n</button>
</div>
)
}
注意事項:
- 使用useContext時,在一個模塊改變數據,另一個模塊是感知不到的
-
setN
會重新渲染<App />
,自上而下逐級通知更新,并不是響應式,因為響應式是監聽數據變化通知對應組件進行更新
useEffect
useEffect鉤子會在每次render后運行
React保證了每次運行useEffect的同時,DOM 都已經更新完畢
應用:
- 作為
componentDidMount
使用,[]作第二個參數 - 作為
componentDidUpdate
使用,可指定依賴 - 作為
componentWillUnmount
使用,通過return - 以上三種可同時存在
function App() {
const [n, setN] = useState(0)
const onClick = () => {
setN(i => i + 1)
}
const afterRender = useEffect;
// componentDidMount
useEffect(() => {
console.log('第一次渲染之后執行這句話')
}, [])
// componentDidUpdate
useEffect(() => {
console.log('每次次都會執行這句話')
})
useEffect(() => {
console.log('n變化就會執行這句話,包含第一次')
}, [n])
// componentWillUnmount
useEffect(() => {
const id = setInterval(() => {
console.log('每一秒都打印這句話')
}, 1000)
return () =>{
// 如果組件多次渲染,則在執行下一個 effect 之前,上一個 effect 就已被清除
console.log('當組件要掛掉了,打印這句話')
window.clearInterval(id)
}
}, [])
return (
<div>
n: {n}
<button onClick={onClick}>+1</button>
</div>
)
}
Hook 允許我們按照代碼的用途分離他們,而不是像生命周期函數那樣
React將按照effect聲明的順序依次調用組件中的每一個effect
對應的,另一個effect鉤子,useLayoutEffect
- useEffect在瀏覽器渲染之后執行,useLayoutEffect在渲染前執行(代碼7)
- useLayoutEffect在渲染前執行,使用它來讀取 DOM 布局并同步觸發重渲染
// 偽代碼
App() -> 執行 -> VDOM -> DOM -> useLayoutEffect -> render -> useEffect
特點:
- useLayoutEffect性能更好,但是會影響用戶看到頁面變化的時間(代碼7)
- useLayoutEffect總是比useEffect先執行
- useLayoutEffect里的任務最好是影響了layout
- 還是推薦優先使用useEffect(如果不涉及操作dom的操作)
為什么建議將修改DOM的操作放到useLayoutEffect里,而不是useEffect呢,是因為當DOM被修改時,瀏覽器的線程處于被阻塞階段(js線程和瀏覽器線程互斥),所以還沒有發生回流、重繪。由于內存中的DOM已經被修改,通過useLayoutEffect可以拿到最新的DOM節點,并且在此時對DOM進行樣式上的修改。這樣修改一次性渲染到屏幕,依舊只有一次回流、重繪的代價。
注意:
由于useEffect是在render之后執行,瀏覽器完成布局和繪制后,不應在函數中執行阻塞瀏覽器更新屏幕的操作
useMemo
React默認有多余的render(修改n,但是依賴m的組件卻自動刷新了),如果props不變就沒有必要再執行一次函數組件,先從一個例子來理解memo(代碼8)
這里有一個問題,如果給子組件一個方法,即使prop沒有變化,子組件還是會每一次都執行
const onClickChild = () => {}
<Child data={m} onClick={onClickChild} />
這是因為在App重新渲染時,生成了新的函數,就像一開始講的多個n的道理一樣,新舊函數雖然功能一樣,但是地址不一樣,這就導致props還是變化了
那么對于子組件的方法,如何重用?
使用useMemo鉤子(代碼9)
const onClickChild = useMemo(() => {
return () => {
console.log(m)
}
}, [m])
特點:
- useMemo第一個參數是
() => value
(value可以是函數、對象之類的),第二個參數是依賴數組[m]
- 只有當依賴變化時,才會重新計算新的value
- 如果依賴沒有變化,就重用之前的value
- 這不就是vue中的
computed
嗎?
注意:
- 如果你的value是個函數,那么你要寫成
useMemo(() => x => console.log(x))
- 這是一個返回函數的函數
- 這么難用的話,用用useCallback
// useMemo
const onClickChild = useMemo(() => {
return () => {
console.log(m)
}
}, [m])
// useCallback
const onClickChild = useCallback(() => {
console.log(m)
})
// 偽代碼
useCallback(x => log(x), [m]) 等價于 useMemo(() => x => log(x), [m])
useMemo
和useCallback
作用完全一樣,語法糖而已
useRef
一直用到的這個例子,每點擊一下就會重新渲染一下App
function App() {
console.log('App 執行');
const [n, setN] = useState(0)
const onClick = () => {
setN(i => i + 1)
}
return (
<div>
<button onClick={onClick}>update n {n}</button>
</div>
)
}
假如我要知道這個App執行了多少次,我怎么記錄?
如果我需要一個值,在組件不斷render的時候也能夠保持不變怎么做?
function App() {
// count的值通過useRef記錄了下來
// 初始化
const count = useRef(0)
useEffect(() => {
// 讀取 count.current
count.current += 1
})
}
同樣的,useRef
也是通過它所對應的fiberNode對象來保存
為什么需要current?
- 為了保證兩次useRef是同一個值,只有引用才能做到
- useRef存儲的實際上是一個對象
{currnt: 0}
,對象對應的是同一個地址(內存) - 每次改變只是改變對象中的值,而不是改變對象,新舊組件必須引用同一個對象
講了useRef
就不得不講講forwardRef
了
在函數組件中怎么使用ref,嘗試一下(代碼10)
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
說明,props無法傳遞ref屬性
所以,函數組件用ref的話,需要用forwardRef
包裝做一下轉發,才能拿到ref
自定義Hook
通過自定義Hook,可以將組件邏輯提取到可重用的函數中
自定義Hook是一個函數,其名稱以 “use” 開頭(符合 Hook 的規則),函數內部可以調用其他的Hook
每次使用自定義 Hook 時,其中的所有 state 和副作用都是完全隔離的(每次調用 Hook,它都會獲取獨立的 state)
代碼