React Hooks
Hook是React v16.8的新特性攻柠,可以用函數(shù)的形式代替原來的繼承類的形式,可以在不編寫
class
的情況下使用state
以及其他React特性
React 設(shè)計原理
- React認(rèn)為崎溃,UI視圖是數(shù)據(jù)的一種視覺映射,
UI = F(Data)
,這里的F
主要負(fù)責(zé)對輸入數(shù)據(jù)進(jìn)行加工另玖,對數(shù)據(jù)變更做出相應(yīng) - 公式里的
F
在React里抽象成組件,React是以組件為粒度編排應(yīng)用的表伦,組件是代碼復(fù)用的最小單元 - 在設(shè)計上谦去,React采用
props
屬性來接收外部數(shù)據(jù),使用state
屬性來管理組件自身產(chǎn)生的數(shù)據(jù)蹦哼,而為了實現(xiàn)(運行時)對數(shù)據(jù)變更做出相應(yīng)需要鳄哭,React采用基于類的組件設(shè)計 - 除此之外,React認(rèn)為組件是有生命周期的纲熏,因此提供了一系列API供開發(fā)者使用
我們所熟悉的React組件長這樣
import React, { Component } from "react";
// React基于Class設(shè)計組件
export default class Button extends Component {
constructor() {
super();
// 組件自身數(shù)據(jù)
this.state = { buttonText: "Click me, please" };
this.handleClick = this.handleClick.bind(this);
}
// 響應(yīng)數(shù)據(jù)變更
handleClick() {
this.setState({ buttonText: "Thanks, been clicked!" });
}
// 編排數(shù)據(jù)呈現(xiàn)UI
render() {
const { buttonText } = this.state;
return <button onClick={this.handleClick}>{buttonText}</button>;
}
}
組件類的缺點
上面實例代碼只是一個按鈕組件妆丘,但是可以看到,它的代碼已經(jīng)很重了局劲。真實的React App由多個類按照層級勺拣,一層層構(gòu)成,復(fù)雜度成倍增長鱼填。再加入 Redux + React Router药有,就變得更復(fù)雜
很可能隨便一個組件最后export
出去就是醬紫的:
export default withStyle(style)(connect(/*something*/)(withRouter(MyComponent)))
一個4層嵌套HOC,嵌套地獄
同時剔氏,如果你的組件內(nèi)事件多塑猖,那么你的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內(nèi)部是當(dāng)做Javascript Function類來處理的羊苟,代碼很難被壓縮,比如方法名稱
還有this
啦感憾,稍微不注意就會出現(xiàn)因this
指向報錯的問題等蜡励。。阻桅。
總結(jié)一下就是:
- 很難復(fù)用邏輯凉倚,會導(dǎo)致組件樹層級很深
- 會產(chǎn)生巨大的組件(很多代碼必須寫在類里面)
- 類組件很難理解,比如方法需要
bind
嫂沉,this
的指向不明確 - 編譯size稽寒,性能問題
Hooks
State Hook
Hook是什么?
可以先通過一個例子來看看趟章,在class中杏糙,我們通過在構(gòu)造函數(shù)中設(shè)置this.state
初始化組件的state:
this.state = {
n: 0
}
而在函數(shù)組件中慎王,我們沒有this
,所以我們不能分配或讀取this.state
宏侍,但是可以在組件中調(diào)用useState
Hook
import React, {useState} from 'react';
function xxx() {
const [n, setN] = useState(0);
}
在上面代碼中赖淤,useState
就是Hook
Hook是一個特殊的函數(shù),它可以讓你“鉤入”React的特性谅河。例如
useState
是允許你在React函數(shù)組件中添加state
的Hook咱旱。
如果你在編寫函數(shù)組件并意識到需要向其添加一些state
,以前的做法是必須將其轉(zhuǎn)化為Class”了#現(xiàn)在你可以在現(xiàn)有的函數(shù)組件中使用Hook
讓函數(shù)組件自身具備狀態(tài)處理能力吐限,且自身能夠通過某種機(jī)制觸發(fā)狀態(tài)的變更并引起re-render,這種機(jī)制就是Hooks
走進(jìn)useState
示例代碼:
import React, { useState } from 'react';
function App() {
// 聲明一個叫 "n" 的 state 變量
// useState接收一個參數(shù)作為初始值
// useState返回一個數(shù)組褂始,[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 />
- 調(diào)用
App
函數(shù)病袄,得到虛擬DOM對象,創(chuàng)建真實DOM - 點擊buttno調(diào)用
setN(n + 1)
赘阀,因為要更新頁面的n
益缠,所以再次render<App />
- 重復(fù)第二步,從控制臺打印看出每次執(zhí)行
setN
都會觸發(fā)App
函數(shù)運行基公,得到一個新的虛擬DOM幅慌,DOM Diff更新真實DOM
那么問題來了,首次運行App
函數(shù)和setN
時都調(diào)用了App
轰豆,兩次運行useState
是一樣的嗎胰伍?setN
改變n
的值了嗎?為什么得到了不一樣的n
酸休,useState
的時候做了什么骂租?
分析:
- setN
- setN一定會修改數(shù)據(jù)x,將n+1存入x
- setN一定會觸發(fā)
<App />
重新渲染(re-render)
- useState
- useState肯定會從x讀取n的最新值
- x
- 每個組件都有自己的數(shù)據(jù)x斑司,我們將其命名為state
嘗試實現(xiàn)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咋整磕潮?由于所有數(shù)據(jù)都放在_state翠胰,產(chǎn)生沖突:
function App() {
const [n, setN] = myUseState(0)
const [m, setM] = myUseState(0)
...
}
解決:
- 把_state做成對象
- 不可行容贝,沒有key,
useState(0)
只傳入了一個參數(shù)0
亡容,并不知道是n
還是m
- 不可行容贝,沒有key,
- 把_state做成數(shù)組
- 可行嗤疯,
_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
調(diào)用順序必須一致茂缚!
- 如果第一次渲染時n是第一個,m是第二個屋谭,k是第三個
- 則第二次渲染時必須保證順序一致脚囊,因為數(shù)組根據(jù)調(diào)用順序存儲值
- re-render時會從第一行代碼開始重新執(zhí)行整個組件
- 所以React不允許出現(xiàn)如下代碼
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會維護(hù)一個虛擬DOM樹我擂,每個節(jié)點都有一個虛擬DOM對象(Fiber)衬以,將_state,index存儲在對象上
額外擴(kuò)展一下Fiber對象校摩,它的數(shù)據(jù)結(jié)構(gòu)如下:
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過程中創(chuàng)建
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應(yīng)該返回的結(jié)果
this.memoizedState = null;
this.firstContextDependency = null;
// ...others
}
總結(jié):
- 每個函數(shù)組件對應(yīng)一個React節(jié)點(FiberNode)
- 每個節(jié)點保存著
_state
(memorizedState)和index
(實際是鏈表) -
useState
會讀取對應(yīng)節(jié)點的state[index] -
index
是由useState
的調(diào)用順序決定 -
setState
會修改_state
,并觸發(fā)更新
搞清楚useState
干了啥以后坤塞,回過頭再看setN
改變n
了嗎冯勉,為什么得到了不一樣的n
(代碼3)
- 先+1,后log => 1
- 先log摹芙,后+1 => 0
- 為什么log出了舊數(shù)據(jù)
分析:
- 先點擊log灼狰,
log(0)
三秒后執(zhí)行,此時n
是0
瘫辩,n
不會變 - 再點擊+1伏嗜,此時調(diào)用的是一個新的函數(shù),生成了新的
n
伐厌,re-render -
n=0
和n=1
同時存在內(nèi)存中
結(jié)論:因為有多個n
承绸,setN
并不會改變n
,React函數(shù)式編程決定了n的值不會被改變挣轨,只會被回收
注意事項:
- 不可局部更新(代碼4)
- 地址要變:
setState(obj)
如果obj
地址不變军熏,那么React就認(rèn)為數(shù)據(jù)沒有變化 - useState接受函數(shù):函數(shù)返回初始state,且只執(zhí)行一次
- setState接收函數(shù):setN(i => i + 1)卷扮,優(yōu)先使用這種形式
useReducer
React本身不提供狀態(tài)管理功能荡澎,通常需要使用外部庫均践,最常用的庫是
Redux
Redux的核心概念是,將需要修改的state都存入到store里摩幔,發(fā)起一個action用來描述發(fā)生了什么彤委,用reducers描述action如何改變state,真正能改變store中數(shù)據(jù)的是store.dispatch API
Reducer是一個純函數(shù)或衡,只承擔(dān)計算 State 的功能焦影,函數(shù)的形式是(state, action) => newState
Action是消息的載體,只能被別人操作封断,自己不能進(jìn)行任何操作
useReducer()
鉤子用來引入Reducer功能(代碼5)
const [state, dispatch] = useReducer(reducer, initial)
上面是useReducer
基本用法
- 接受Reducer函數(shù)和一個初始值作為參數(shù)
- 返回一個數(shù)組斯辰,數(shù)組
[0]
位是狀態(tài)當(dāng)前值,第[1]
位是dispatch
函數(shù)坡疼,用來發(fā)送action
似曾相識的感覺
const [n, setN] = useState(0)
// n:讀
// setN:寫
總的來說useReducer就是復(fù)雜版本的useState彬呻,那么什么時候使用useReducer,什么時候又使用useState呢柄瑰?
看一個代碼6
當(dāng)你需要維護(hù)多個state闸氮,那么為什么不用一個對象來維護(hù)呢,對象是可以合并的
需要注意的是教沾,由于Hooks可以提供狀態(tài)管理和Reducer函數(shù)湖苞,所以在這方面可以取代Redux。但是详囤,它沒法兒提供中間件(midddleware)和時間旅行(time travel),如果你需要這兩個功能镐作,還是要用Redux藏姐。
中間件原理:封裝改造store.dispatch,將其指向中間件该贾,以實現(xiàn)在dispatch和reducer之間處理action數(shù)據(jù)的邏輯羔杨,也可以將中間件看成是dispatch方法的封裝器
有沒有代替Redux的方法呢?
Reducer + Context
useContext
什么是上下文杨蛋?
- 全局變量是全局的上下文
- 上下文是局部的全局變量
使用方法:
// 創(chuàng)建上下文
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返回的是對象兜材,不是數(shù)組
const {n, setN} = useContext(c)
const onClick = () => {
setN( i => i + 1)
}
return (
<div>我是兒子,我可以拿到n:{n}
<button onClick={onClick}>我也可以更新n</button>
</div>
)
}
注意事項:
- 使用useContext時逞力,在一個模塊改變數(shù)據(jù)曙寡,另一個模塊是感知不到的
-
setN
會重新渲染<App />
,自上而下逐級通知更新寇荧,并不是響應(yīng)式举庶,因為響應(yīng)式是監(jiān)聽數(shù)據(jù)變化通知對應(yīng)組件進(jìn)行更新
useEffect
useEffect鉤子會在每次render后運行
React保證了每次運行useEffect的同時,DOM 都已經(jīng)更新完畢
應(yīng)用:
- 作為
componentDidMount
使用揩抡,[]作第二個參數(shù) - 作為
componentDidUpdate
使用户侥,可指定依賴 - 作為
componentWillUnmount
使用镀琉,通過return - 以上三種可同時存在
function App() {
const [n, setN] = useState(0)
const onClick = () => {
setN(i => i + 1)
}
const afterRender = useEffect;
// componentDidMount
useEffect(() => {
console.log('第一次渲染之后執(zhí)行這句話')
}, [])
// componentDidUpdate
useEffect(() => {
console.log('每次次都會執(zhí)行這句話')
})
useEffect(() => {
console.log('n變化就會執(zhí)行這句話,包含第一次')
}, [n])
// componentWillUnmount
useEffect(() => {
const id = setInterval(() => {
console.log('每一秒都打印這句話')
}, 1000)
return () =>{
// 如果組件多次渲染蕊唐,則在執(zhí)行下一個 effect 之前屋摔,上一個 effect 就已被清除
console.log('當(dāng)組件要掛掉了,打印這句話')
window.clearInterval(id)
}
}, [])
return (
<div>
n: {n}
<button onClick={onClick}>+1</button>
</div>
)
}
Hook 允許我們按照代碼的用途分離他們替梨,而不是像生命周期函數(shù)那樣
React將按照effect聲明的順序依次調(diào)用組件中的每一個effect
對應(yīng)的钓试,另一個effect鉤子,useLayoutEffect
- useEffect在瀏覽器渲染之后執(zhí)行耙替,useLayoutEffect在渲染前執(zhí)行(代碼7)
- useLayoutEffect在渲染前執(zhí)行亚侠,使用它來讀取 DOM 布局并同步觸發(fā)重渲染
// 偽代碼
App() -> 執(zhí)行 -> VDOM -> DOM -> useLayoutEffect -> render -> useEffect
特點:
- useLayoutEffect性能更好,但是會影響用戶看到頁面變化的時間(代碼7)
- useLayoutEffect總是比useEffect先執(zhí)行
- useLayoutEffect里的任務(wù)最好是影響了layout
- 還是推薦優(yōu)先使用useEffect(如果不涉及操作dom的操作)
為什么建議將修改DOM的操作放到useLayoutEffect里俗扇,而不是useEffect呢硝烂,是因為當(dāng)DOM被修改時,瀏覽器的線程處于被阻塞階段(js線程和瀏覽器線程互斥)铜幽,所以還沒有發(fā)生回流滞谢、重繪。由于內(nèi)存中的DOM已經(jīng)被修改除抛,通過useLayoutEffect可以拿到最新的DOM節(jié)點狮杨,并且在此時對DOM進(jìn)行樣式上的修改。這樣修改一次性渲染到屏幕到忽,依舊只有一次回流橄教、重繪的代價。
注意:
由于useEffect是在render之后執(zhí)行喘漏,瀏覽器完成布局和繪制后护蝶,不應(yīng)在函數(shù)中執(zhí)行阻塞瀏覽器更新屏幕的操作
useMemo
React默認(rèn)有多余的render(修改n,但是依賴m的組件卻自動刷新了)翩迈,如果props不變就沒有必要再執(zhí)行一次函數(shù)組件持灰,先從一個例子來理解memo(代碼8)
這里有一個問題,如果給子組件一個方法负饲,即使prop沒有變化堤魁,子組件還是會每一次都執(zhí)行
const onClickChild = () => {}
<Child data={m} onClick={onClickChild} />
這是因為在App重新渲染時,生成了新的函數(shù)返十,就像一開始講的多個n的道理一樣妥泉,新舊函數(shù)雖然功能一樣,但是地址不一樣洞坑,這就導(dǎo)致props還是變化了
那么對于子組件的方法涛漂,如何重用?
使用useMemo鉤子(代碼9)
const onClickChild = useMemo(() => {
return () => {
console.log(m)
}
}, [m])
特點:
- useMemo第一個參數(shù)是
() => value
(value可以是函數(shù)、對象之類的),第二個參數(shù)是依賴數(shù)組[m]
- 只有當(dāng)依賴變化時匈仗,才會重新計算新的value
- 如果依賴沒有變化瓢剿,就重用之前的value
- 這不就是vue中的
computed
嗎?
注意:
- 如果你的value是個函數(shù)悠轩,那么你要寫成
useMemo(() => x => console.log(x))
- 這是一個返回函數(shù)的函數(shù)
- 這么難用的話间狂,用用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 執(zhí)行');
const [n, setN] = useState(0)
const onClick = () => {
setN(i => i + 1)
}
return (
<div>
<button onClick={onClick}>update n {n}</button>
</div>
)
}
假如我要知道這個App執(zhí)行了多少次鉴象,我怎么記錄?
如果我需要一個值何鸡,在組件不斷render的時候也能夠保持不變怎么做纺弊?
function App() {
// count的值通過useRef記錄了下來
// 初始化
const count = useRef(0)
useEffect(() => {
// 讀取 count.current
count.current += 1
})
}
同樣的,useRef
也是通過它所對應(yīng)的fiberNode對象來保存
為什么需要current骡男?
- 為了保證兩次useRef是同一個值淆游,只有引用才能做到
- useRef存儲的實際上是一個對象
{currnt: 0}
,對象對應(yīng)的是同一個地址(內(nèi)存) - 每次改變只是改變對象中的值隔盛,而不是改變對象犹菱,新舊組件必須引用同一個對象
講了useRef
就不得不講講forwardRef
了
在函數(shù)組件中怎么使用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屬性
所以腊脱,函數(shù)組件用ref的話,需要用forwardRef
包裝做一下轉(zhuǎn)發(fā)龙亲,才能拿到ref
自定義Hook
通過自定義Hook陕凹,可以將組件邏輯提取到可重用的函數(shù)中
自定義Hook是一個函數(shù),其名稱以 “use” 開頭(符合 Hook 的規(guī)則)鳄炉,函數(shù)內(nèi)部可以調(diào)用其他的Hook
每次使用自定義 Hook 時捆姜,其中的所有 state 和副作用都是完全隔離的(每次調(diào)用 Hook,它都會獲取獨立的 state)
代碼